Bug#1121737: bookworm-pu: package unbound/1.17.1-2+deb12u4
Package: release.debian.org
Severity: normal
Tags: bookworm
X-Debbugs-Cc: unbound@packages.debian.org
Control: affects -1 + src:unbound
User: release.debian.org@packages.debian.org
Usertags: pu
[ Reason ]
There are a few defects known in unbound as currently exists
in debian bookworm, version 1.17.1-2+deb12u3, including one
security (CVE-2025-11411, possible domain hijacking).
Other issues are:
correctness, https://github.com/NLnetLabs/unbound/issues/823
possible participation in DDoS amplification attacks
robustness, unbound-anchor loses info when filesystem is full
(#1100870)
[ Tests ]
This version of unbound has been verified to work in a regular
scenario, it is used in production in our office now. Also it
passes all upstream tests.
[ Risks ]
Since I had to back-port upstream changes to earlier versions,
and there were enough changes so a backport was needed in the
first place, there's a risk I did something wrong in the process.
And while I'm confident enough to put the result into production,
there's no 100% guarantee nothing will break.
>From this PoV, it'd be nice to have this version of unbound
package available in s-p-u for some time, at least.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
[ Changes ]
The changes are rather fun this time. In the previous release
of unbound for bookworm, two CVEs has been fixed by a single
patch which is a combination of two upstream commits adjusted
to older version of unbound. This single patch had no metadata.
This is CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
When I started back-porting further fixes, I had to return to
a few older upstream changes, since they were touching the same
areas or introducing some functions or arguments which are used
in the subsequent fixes which I'm back-porting. And these extra
older changes introduces code which were modified by the above
2-CVEs-fixing patch, - these modifications were omitted.
So I had to re-do that combined patch on top of picked-up
earlier changes. I split it into two, so it now corresponds
to the 2 upstream commits, added metadata, and included parts
which were missing previously (mostly testdata/ changes).
All this, plus the size of fixes for CVE-2025-11411, resulted
in a rather large debdiff, and it's rather difficult to review
the changes between that single combined patch and two separate
patches. But it's now much easier to compare with the upstream
changes.
Additionally, I reviewed all other changes included in the
previous upload, - all looks ok, but I found one hunk which doesn't
belong to the patch where it contains - CVE-2024-33655.patch has
an unrelated change to testdata/ which is not included in the
upstream commit. I dunno where it came from.
And finally, while working on CVE-2025-11411, I had to pick a
single small change from a larger upstream commit (passing an
extra argument to a single function call) in a separate patch.
The resulting d/changelog entry:
unbound (1.17.1-2+deb12u4) bookworm; urgency=medium
* CVE-2024-33655.patch: remove unrelated change
testdata/fwd_udptmout.tdir/fwd_udptmout.conf is not modified
by the upstream commit in question (c3206f4568f6)
* fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch
Fixes: https://github.com/NLnetLabs/unbound/issues/823
* fix-not-following-cleared-RD-flags-amplification.patch
fix potential amplification DDoS attacks
* replace combined CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
with 2 separate upstream commits, add patch descriptions, and add
missing changes for testdata files:
o CVE-2023-50387-DNSSEC-verification-complexity.patch
o CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
* 3 changes to fix CVE-2025-11411 (possible domain hijacking attack):
o 1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch
(a change from "Add harden-unknown-additional option" upstream patch)
o 2-possible-domain-hijacking-attack.patch
o 3-additional-fix-for-possible-domain-hijacking.patch
(Closes: #1121446)
* fix-595-unbound-anchor-cannot-deal-with-full-disk.patch
Fixes: https://github.com/NLnetLabs/unbound/issues/595
(Closes: #1100870)
* d/gbp.conf: set default branch to debian/bookworm
-- Michael Tokarev <mjt@tls.msk.ru> Sun, 30 Nov 2025 13:33:55 +0300
It all looks rather big and messy, but the result is quite nice.
I very much hope the above explanations will help to review unbound
changes.
Thanks,
/mjt
diff -Nru unbound-1.17.1/debian/changelog unbound-1.17.1/debian/changelog
--- unbound-1.17.1/debian/changelog 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/changelog 2025-11-30 13:33:55.000000000 +0300
@@ -1,3 +1,30 @@
+unbound (1.17.1-2+deb12u4) bookworm; urgency=medium
+
+ * CVE-2024-33655.patch: remove unrelated change
+ testdata/fwd_udptmout.tdir/fwd_udptmout.conf is not modified
+ by the upstream commit in question (c3206f4568f6)
+ * fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch
+ Fixes: https://github.com/NLnetLabs/unbound/issues/823
+ * fix-not-following-cleared-RD-flags-amplification.patch
+ fix potential amplification DDoS attacks
+ * replace combined CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
+ with 2 separate upstream commits, add patch descriptions, and add
+ missing changes for testdata files:
+ o CVE-2023-50387-DNSSEC-verification-complexity.patch
+ o CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
+ * 3 changes to fix CVE-2025-11411 (possible domain hijacking attack):
+ o 1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch
+ (a change from "Add harden-unknown-additional option" upstream patch)
+ o 2-possible-domain-hijacking-attack.patch
+ o 3-additional-fix-for-possible-domain-hijacking.patch
+ (Closes: #1121446)
+ * fix-595-unbound-anchor-cannot-deal-with-full-disk.patch
+ Fixes: https://github.com/NLnetLabs/unbound/issues/595
+ (Closes: #1100870)
+ * d/gbp.conf: set default branch to debian/bookworm
+
+ -- Michael Tokarev <mjt@tls.msk.ru> Sun, 30 Nov 2025 13:33:55 +0300
+
unbound (1.17.1-2+deb12u3) bookworm-security; urgency=high
* Non-maintainer upload.
diff -Nru unbound-1.17.1/debian/gbp.conf unbound-1.17.1/debian/gbp.conf
--- unbound-1.17.1/debian/gbp.conf 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/gbp.conf 2025-11-29 13:14:16.000000000 +0300
@@ -1,3 +1,6 @@
+[DEFAULT]
+debian-branch = debian/bookworm
+
[buildpackage]
pristine-tar = True
diff -Nru unbound-1.17.1/debian/patches/CVE-2023-50387-DNSSEC-verification-complexity.patch unbound-1.17.1/debian/patches/CVE-2023-50387-DNSSEC-verification-complexity.patch
--- unbound-1.17.1/debian/patches/CVE-2023-50387-DNSSEC-verification-complexity.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2023-50387-DNSSEC-verification-complexity.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,782 @@
+From: "W.C.A. Wijngaards" <wouter@nlnetlabs.nl>
+Date: Tue, 13 Feb 2024 13:02:08 +0100
+Subject: Fix CVE-2023-50387, DNSSEC verification complexity can be exploited to
+ exhaust CPU resources and stall DNS resolvers
+
+Origin: https://github.com/NLnetLabs/unbound/commit/882903f2fa800c4cb6f5e225b728e2887bb7b9ae
+Bug: https://nlnetlabs.nl/downloads/unbound/CVE-2023-50387.txt
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-50387
+---
+ services/authzone.c | 3 +-
+ testcode/unitverify.c | 3 +-
+ testdata/val_any.rpl | 3 +
+ testdata/val_any_dname.rpl | 3 +
+ testdata/val_any_negcache.rpl | 3 +
+ util/fptr_wlist.c | 1 +
+ validator/val_nsec.c | 3 +-
+ validator/val_nsec3.c | 4 +-
+ validator/val_sigcrypt.c | 37 ++++++-
+ validator/val_sigcrypt.h | 3 +-
+ validator/val_utils.c | 22 +++-
+ validator/val_utils.h | 4 +-
+ validator/validator.c | 186 +++++++++++++++++++++++++++++++---
+ validator/validator.h | 13 +++
+ 14 files changed, 259 insertions(+), 29 deletions(-)
+
+diff --git a/services/authzone.c b/services/authzone.c
+index 3898767c7..4c63b2e0f 100644
+--- a/services/authzone.c
++++ b/services/authzone.c
+@@ -7767,6 +7767,7 @@ static int zonemd_dnssec_verify_rrset(struct auth_zone* z,
+ enum sec_status sec;
+ struct val_env* ve;
+ int m;
++ int verified = 0;
+ m = modstack_find(mods, "validator");
+ if(m == -1) {
+ auth_zone_log(z->name, VERB_ALGO, "zonemd dnssec verify: have "
+@@ -7790,7 +7791,7 @@ static int zonemd_dnssec_verify_rrset(struct auth_zone* z,
+ "zonemd: verify %s RRset with DNSKEY", typestr);
+ }
+ sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg, why_bogus, NULL,
+- LDNS_SECTION_ANSWER, NULL);
++ LDNS_SECTION_ANSWER, NULL, &verified);
+ if(sec == sec_status_secure) {
+ return 1;
+ }
+diff --git a/testcode/unitverify.c b/testcode/unitverify.c
+index ff069a1bb..fb7d84467 100644
+--- a/testcode/unitverify.c
++++ b/testcode/unitverify.c
+@@ -180,6 +180,7 @@ verifytest_rrset(struct module_env* env, struct val_env* ve,
+ enum sec_status sec;
+ char* reason = NULL;
+ uint8_t sigalg[ALGO_NEEDS_MAX+1];
++ int verified = 0;
+ if(vsig) {
+ log_nametypeclass(VERB_QUERY, "verify of rrset",
+ rrset->rk.dname, ntohs(rrset->rk.type),
+@@ -188,7 +189,7 @@ verifytest_rrset(struct module_env* env, struct val_env* ve,
+ setup_sigalg(dnskey, sigalg); /* check all algorithms in the dnskey */
+ /* ok to give null as qstate here, won't be used for answer section. */
+ sec = dnskeyset_verify_rrset(env, ve, rrset, dnskey, sigalg, &reason, NULL,
+- LDNS_SECTION_ANSWER, NULL);
++ LDNS_SECTION_ANSWER, NULL, &verified);
+ if(vsig) {
+ printf("verify outcome is: %s %s\n", sec_status_to_string(sec),
+ reason?reason:"");
+diff --git a/testdata/val_any.rpl b/testdata/val_any.rpl
+index 7d94094ce..5b91ecd5b 100644
+--- a/testdata/val_any.rpl
++++ b/testdata/val_any.rpl
+@@ -161,6 +161,9 @@ SECTION QUESTION
+ example.com. IN ANY
+ ENTRY_END
+
++; Allow validation resuming for the RRSIGs
++STEP 2 TIME_PASSES ELAPSE 0.05
++
+ ; recursion happens here.
+ STEP 10 CHECK_ANSWER
+ ENTRY_BEGIN
+diff --git a/testdata/val_any_dname.rpl b/testdata/val_any_dname.rpl
+index ff06de1eb..c80a91229 100644
+--- a/testdata/val_any_dname.rpl
++++ b/testdata/val_any_dname.rpl
+@@ -165,6 +165,9 @@ SECTION QUESTION
+ example.com. IN ANY
+ ENTRY_END
+
++; Allow validation resuming for the RRSIGs
++STEP 2 TIME_PASSES ELAPSE 0.05
++
+ ; recursion happens here.
+ STEP 10 CHECK_ANSWER
+ ENTRY_BEGIN
+diff --git a/testdata/val_any_negcache.rpl b/testdata/val_any_negcache.rpl
+index 1f04dd885..3514c1f86 100644
+--- a/testdata/val_any_negcache.rpl
++++ b/testdata/val_any_negcache.rpl
+@@ -198,6 +198,9 @@ SECTION QUESTION
+ example.com. IN ANY
+ ENTRY_END
+
++; Allow validation resuming for the RRSIGs
++STEP 21 TIME_PASSES ELAPSE 0.05
++
+ ; recursion happens here.
+ STEP 30 CHECK_ANSWER
+ ENTRY_BEGIN
+diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c
+index dc8ab6693..e927e05a6 100644
+--- a/util/fptr_wlist.c
++++ b/util/fptr_wlist.c
+@@ -131,6 +131,7 @@ fptr_whitelist_comm_timer(void (*fptr)(void*))
+ else if(fptr == &pending_udp_timer_delay_cb) return 1;
+ else if(fptr == &worker_stat_timer_cb) return 1;
+ else if(fptr == &worker_probe_timer_cb) return 1;
++ else if(fptr == &validate_msg_signatures_timer_cb) return 1;
+ #ifdef UB_ON_WINDOWS
+ else if(fptr == &wsvc_cron_cb) return 1;
+ #endif
+diff --git a/validator/val_nsec.c b/validator/val_nsec.c
+index 876bfab6d..5871db90e 100644
+--- a/validator/val_nsec.c
++++ b/validator/val_nsec.c
+@@ -180,6 +180,7 @@ nsec_verify_rrset(struct module_env* env, struct val_env* ve,
+ {
+ struct packed_rrset_data* d = (struct packed_rrset_data*)
+ nsec->entry.data;
++ int verified = 0;
+ if(!d) return 0;
+ if(d->security == sec_status_secure)
+ return 1;
+@@ -187,7 +188,7 @@ nsec_verify_rrset(struct module_env* env, struct val_env* ve,
+ if(d->security == sec_status_secure)
+ return 1;
+ d->security = val_verify_rrset_entry(env, ve, nsec, kkey, reason,
+- NULL, LDNS_SECTION_AUTHORITY, qstate);
++ NULL, LDNS_SECTION_AUTHORITY, qstate, &verified);
+ if(d->security == sec_status_secure) {
+ rrset_update_sec_status(env->rrset_cache, nsec, *env->now);
+ return 1;
+diff --git a/validator/val_nsec3.c b/validator/val_nsec3.c
+index a2b3794f6..f4b9b2bca 100644
+--- a/validator/val_nsec3.c
++++ b/validator/val_nsec3.c
+@@ -1294,6 +1294,7 @@ list_is_secure(struct module_env* env, struct val_env* ve,
+ {
+ struct packed_rrset_data* d;
+ size_t i;
++ int verified = 0;
+ for(i=0; i<num; i++) {
+ d = (struct packed_rrset_data*)list[i]->entry.data;
+ if(list[i]->rk.type != htons(LDNS_RR_TYPE_NSEC3))
+@@ -1304,7 +1305,8 @@ list_is_secure(struct module_env* env, struct val_env* ve,
+ if(d->security == sec_status_secure)
+ continue;
+ d->security = val_verify_rrset_entry(env, ve, list[i], kkey,
+- reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
++ reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
++ &verified);
+ if(d->security != sec_status_secure) {
+ verbose(VERB_ALGO, "NSEC3 did not verify");
+ return 0;
+diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c
+index 5ab21e20e..8600a6821 100644
+--- a/validator/val_sigcrypt.c
++++ b/validator/val_sigcrypt.c
+@@ -78,6 +78,9 @@
+ #include <openssl/engine.h>
+ #endif
+
++/** Maximum number of RRSIG validations for an RRset. */
++#define MAX_VALIDATE_RRSIGS 8
++
+ /** return number of rrs in an rrset */
+ static size_t
+ rrset_get_count(struct ub_packed_rrset_key* rrset)
+@@ -541,6 +544,8 @@ int algo_needs_missing(struct algo_needs* n)
+ * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
+ * @param section: section of packet where this rrset comes from.
+ * @param qstate: qstate with region.
++ * @param numverified: incremented when the number of RRSIG validations
++ * increases.
+ * @return secure if any key signs *this* signature. bogus if no key signs it,
+ * unchecked on error, or indeterminate if all keys are not supported by
+ * the crypto library (openssl3+ only).
+@@ -551,7 +556,8 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key* dnskey, size_t sig_idx,
+ struct rbtree_type** sortree,
+ char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate)
++ sldns_pkt_section section, struct module_qstate* qstate,
++ int* numverified)
+ {
+ /* find matching keys and check them */
+ enum sec_status sec = sec_status_bogus;
+@@ -575,6 +581,7 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
+ tag != dnskey_calc_keytag(dnskey, i))
+ continue;
+ numchecked ++;
++ (*numverified)++;
+
+ /* see if key verifies */
+ sec = dnskey_verify_rrset_sig(env->scratch,
+@@ -585,6 +592,13 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
+ return sec;
+ else if(sec == sec_status_indeterminate)
+ numindeterminate ++;
++ if(*numverified > MAX_VALIDATE_RRSIGS) {
++ *reason = "too many RRSIG validations";
++ if(reason_bogus)
++ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
++ verbose(VERB_ALGO, "verify sig: too many RRSIG validations");
++ return sec_status_bogus;
++ }
+ }
+ if(numchecked == 0) {
+ *reason = "signatures from unknown keys";
+@@ -608,7 +622,7 @@ enum sec_status
+ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* dnskey,
+ uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate)
++ sldns_pkt_section section, struct module_qstate* qstate, int* verified)
+ {
+ enum sec_status sec;
+ size_t i, num;
+@@ -616,6 +630,7 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
+ /* make sure that for all DNSKEY algorithms there are valid sigs */
+ struct algo_needs needs;
+ int alg;
++ *verified = 0;
+
+ num = rrset_get_sigcount(rrset);
+ if(num == 0) {
+@@ -640,7 +655,7 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
+ for(i=0; i<num; i++) {
+ sec = dnskeyset_verify_rrset_sig(env, ve, *env->now, rrset,
+ dnskey, i, &sortree, reason, reason_bogus,
+- section, qstate);
++ section, qstate, verified);
+ /* see which algorithm has been fixed up */
+ if(sec == sec_status_secure) {
+ if(!sigalg)
+@@ -652,6 +667,13 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
+ algo_needs_set_bogus(&needs,
+ (uint8_t)rrset_get_sig_algo(rrset, i));
+ }
++ if(*verified > MAX_VALIDATE_RRSIGS) {
++ verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
++ *reason = "too many RRSIG validations";
++ if(reason_bogus)
++ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
++ return sec_status_bogus;
++ }
+ }
+ if(sigalg && (alg=algo_needs_missing(&needs)) != 0) {
+ verbose(VERB_ALGO, "rrset failed to verify: "
+@@ -690,6 +712,7 @@ dnskey_verify_rrset(struct module_env* env, struct val_env* ve,
+ int buf_canon = 0;
+ uint16_t tag = dnskey_calc_keytag(dnskey, dnskey_idx);
+ int algo = dnskey_get_algo(dnskey, dnskey_idx);
++ int numverified = 0;
+
+ num = rrset_get_sigcount(rrset);
+ if(num == 0) {
+@@ -713,8 +736,16 @@ dnskey_verify_rrset(struct module_env* env, struct val_env* ve,
+ if(sec == sec_status_secure)
+ return sec;
+ numchecked ++;
++ numverified ++;
+ if(sec == sec_status_indeterminate)
+ numindeterminate ++;
++ if(numverified > MAX_VALIDATE_RRSIGS) {
++ verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
++ *reason = "too many RRSIG validations";
++ if(reason_bogus)
++ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
++ return sec_status_bogus;
++ }
+ }
+ verbose(VERB_ALGO, "rrset failed to verify: all signatures are bogus");
+ if(!numchecked) {
+diff --git a/validator/val_sigcrypt.h b/validator/val_sigcrypt.h
+index 7f52b71e4..1a3d8fcb2 100644
+--- a/validator/val_sigcrypt.h
++++ b/validator/val_sigcrypt.h
+@@ -260,6 +260,7 @@ uint16_t dnskey_get_flags(struct ub_packed_rrset_key* k, size_t idx);
+ * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
+ * @param section: section of packet where this rrset comes from.
+ * @param qstate: qstate with region.
++ * @param verified: if not NULL the number of RRSIG validations is returned.
+ * @return SECURE if one key in the set verifies one rrsig.
+ * UNCHECKED on allocation errors, unsupported algorithms, malformed data,
+ * and BOGUS on verification failures (no keys match any signatures).
+@@ -268,7 +269,7 @@ enum sec_status dnskeyset_verify_rrset(struct module_env* env,
+ struct val_env* ve, struct ub_packed_rrset_key* rrset,
+ struct ub_packed_rrset_key* dnskey, uint8_t* sigalg,
+ char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate);
++ sldns_pkt_section section, struct module_qstate* qstate, int* verified);
+
+
+ /**
+diff --git a/validator/val_utils.c b/validator/val_utils.c
+index e2319ee23..cb37ea00e 100644
+--- a/validator/val_utils.c
++++ b/validator/val_utils.c
+@@ -58,6 +58,10 @@
+ #include "sldns/wire2str.h"
+ #include "sldns/parseutil.h"
+
++/** Maximum allowed digest match failures per DS, for DNSKEYs with the same
++ * properties */
++#define MAX_DS_MATCH_FAILURES 4
++
+ enum val_classification
+ val_classify_response(uint16_t query_flags, struct query_info* origqinf,
+ struct query_info* qinf, struct reply_info* rep, size_t skip)
+@@ -336,7 +340,8 @@ static enum sec_status
+ val_verify_rrset(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* keys,
+ uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate)
++ sldns_pkt_section section, struct module_qstate* qstate,
++ int *verified)
+ {
+ enum sec_status sec;
+ struct packed_rrset_data* d = (struct packed_rrset_data*)rrset->
+@@ -346,6 +351,7 @@ val_verify_rrset(struct module_env* env, struct val_env* ve,
+ log_nametypeclass(VERB_ALGO, "verify rrset cached",
+ rrset->rk.dname, ntohs(rrset->rk.type),
+ ntohs(rrset->rk.rrset_class));
++ *verified = 0;
+ return d->security;
+ }
+ /* check in the cache if verification has already been done */
+@@ -354,12 +360,13 @@ val_verify_rrset(struct module_env* env, struct val_env* ve,
+ log_nametypeclass(VERB_ALGO, "verify rrset from cache",
+ rrset->rk.dname, ntohs(rrset->rk.type),
+ ntohs(rrset->rk.rrset_class));
++ *verified = 0;
+ return d->security;
+ }
+ log_nametypeclass(VERB_ALGO, "verify rrset", rrset->rk.dname,
+ ntohs(rrset->rk.type), ntohs(rrset->rk.rrset_class));
+ sec = dnskeyset_verify_rrset(env, ve, rrset, keys, sigalg, reason,
+- reason_bogus, section, qstate);
++ reason_bogus, section, qstate, verified);
+ verbose(VERB_ALGO, "verify result: %s", sec_status_to_string(sec));
+ regional_free_all(env->scratch);
+
+@@ -393,7 +400,8 @@ enum sec_status
+ val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey,
+ char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate)
++ sldns_pkt_section section, struct module_qstate* qstate,
++ int* verified)
+ {
+ /* temporary dnskey rrset-key */
+ struct ub_packed_rrset_key dnskey;
+@@ -407,7 +415,7 @@ val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
+ dnskey.entry.key = &dnskey;
+ dnskey.entry.data = kd->rrset_data;
+ sec = val_verify_rrset(env, ve, rrset, &dnskey, kd->algo, reason,
+- reason_bogus, section, qstate);
++ reason_bogus, section, qstate, verified);
+ return sec;
+ }
+
+@@ -439,6 +447,12 @@ verify_dnskeys_with_ds_rr(struct module_env* env, struct val_env* ve,
+ if(!ds_digest_match_dnskey(env, dnskey_rrset, i, ds_rrset,
+ ds_idx)) {
+ verbose(VERB_ALGO, "DS match attempt failed");
++ if(numchecked > numhashok + MAX_DS_MATCH_FAILURES) {
++ verbose(VERB_ALGO, "DS match attempt reached "
++ "MAX_DS_MATCH_FAILURES (%d); bogus",
++ MAX_DS_MATCH_FAILURES);
++ return sec_status_bogus;
++ }
+ continue;
+ }
+ numhashok++;
+diff --git a/validator/val_utils.h b/validator/val_utils.h
+index 83e3d0ad8..e8cdcefa6 100644
+--- a/validator/val_utils.h
++++ b/validator/val_utils.h
+@@ -124,12 +124,14 @@ void val_find_signer(enum val_classification subtype,
+ * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
+ * @param section: section of packet where this rrset comes from.
+ * @param qstate: qstate with region.
++ * @param verified: if not NULL, the number of RRSIG validations is returned.
+ * @return security status of verification.
+ */
+ enum sec_status val_verify_rrset_entry(struct module_env* env,
+ struct val_env* ve, struct ub_packed_rrset_key* rrset,
+ struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus,
+- sldns_pkt_section section, struct module_qstate* qstate);
++ sldns_pkt_section section, struct module_qstate* qstate,
++ int* verified);
+
+ /**
+ * Verify DNSKEYs with DS rrset. Like val_verify_new_DNSKEYs but
+diff --git a/validator/validator.c b/validator/validator.c
+index 1723afefe..a4549c00b 100644
+--- a/validator/validator.c
++++ b/validator/validator.c
+@@ -64,6 +64,11 @@
+ #include "sldns/wire2str.h"
+ #include "sldns/str2wire.h"
+
++/** Max number of RRSIGs to validate at once, suspend query for later. */
++#define MAX_VALIDATE_AT_ONCE 8
++/** Max number of validation suspends allowed, error out otherwise. */
++#define MAX_VALIDATION_SUSPENDS 16
++
+ /* forward decl for cache response and normal super inform calls of a DS */
+ static void process_ds_response(struct module_qstate* qstate,
+ struct val_qstate* vq, int id, int rcode, struct dns_msg* msg,
+@@ -281,6 +286,21 @@ val_new(struct module_qstate* qstate, int id)
+ return val_new_getmsg(qstate, vq);
+ }
+
++/** reset validator query state for query restart */
++static void
++val_restart(struct val_qstate* vq)
++{
++ struct comm_timer* temp_timer;
++ int restart_count;
++ if(!vq) return;
++ temp_timer = vq->msg_signatures_timer;
++ restart_count = vq->restart_count+1;
++ memset(vq, 0, sizeof(*vq));
++ vq->msg_signatures_timer = temp_timer;
++ vq->restart_count = restart_count;
++ vq->state = VAL_INIT_STATE;
++}
++
+ /**
+ * Exit validation with an error status
+ *
+@@ -587,30 +607,42 @@ prime_trust_anchor(struct module_qstate* qstate, struct val_qstate* vq,
+ * completed.
+ *
+ * @param qstate: query state.
++ * @param vq: validator query state.
+ * @param env: module env for verify.
+ * @param ve: validator env for verify.
+ * @param qchase: query that was made.
+ * @param chase_reply: answer to validate.
+ * @param key_entry: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param suspend: returned true if the task takes to long and needs to
++ * suspend to continue the effort later.
+ * @return false if any of the rrsets in the an or ns sections of the message
+ * fail to verify. The message is then set to bogus.
+ */
+ static int
+-validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
+- struct val_env* ve, struct query_info* qchase,
+- struct reply_info* chase_reply, struct key_entry_key* key_entry)
++validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq,
++ struct module_env* env, struct val_env* ve, struct query_info* qchase,
++ struct reply_info* chase_reply, struct key_entry_key* key_entry,
++ int* suspend)
+ {
+ uint8_t* sname;
+ size_t i, slen;
+ struct ub_packed_rrset_key* s;
+ enum sec_status sec;
+- int dname_seen = 0;
++ int dname_seen = 0, num_verifies = 0, verified, have_state = 0;
+ char* reason = NULL;
+ sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
++ *suspend = 0;
++ if(vq->msg_signatures_state) {
++ /* Pick up the state, and reset it, may not be needed now. */
++ vq->msg_signatures_state = 0;
++ have_state = 1;
++ }
+
+ /* validate the ANSWER section */
+ for(i=0; i<chase_reply->an_numrrsets; i++) {
++ if(have_state && i <= vq->msg_signatures_index)
++ continue;
+ s = chase_reply->rrsets[i];
+ /* Skip the CNAME following a (validated) DNAME.
+ * Because of the normalization routines in the iterator,
+@@ -629,7 +661,7 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
+
+ /* Verify the answer rrset */
+ sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
+- &reason_bogus, LDNS_SECTION_ANSWER, qstate);
++ &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
+ /* If the (answer) rrset failed to validate, then this
+ * message is BAD. */
+ if(sec != sec_status_secure) {
+@@ -654,14 +686,33 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
+ ntohs(s->rk.type) == LDNS_RR_TYPE_DNAME) {
+ dname_seen = 1;
+ }
++ num_verifies += verified;
++ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
++ i+1 < (env->cfg->val_clean_additional?
++ chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
++ chase_reply->rrset_count)) {
++ /* If the number of RRSIGs exceeds the maximum in
++ * one go, suspend. Only suspend if there is a next
++ * rrset to verify, i+1<loopmax. Store where to
++ * continue later. */
++ *suspend = 1;
++ vq->msg_signatures_state = 1;
++ vq->msg_signatures_index = i;
++ verbose(VERB_ALGO, "msg signature validation "
++ "suspended");
++ return 0;
++ }
+ }
+
+ /* validate the AUTHORITY section */
+ for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
+ chase_reply->ns_numrrsets; i++) {
++ if(have_state && i <= vq->msg_signatures_index)
++ continue;
+ s = chase_reply->rrsets[i];
+ sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
+- &reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
++ &reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
++ &verified);
+ /* If anything in the authority section fails to be secure,
+ * we have a bad message. */
+ if(sec != sec_status_secure) {
+@@ -675,6 +726,18 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
+ update_reason_bogus(chase_reply, reason_bogus);
+ return 0;
+ }
++ num_verifies += verified;
++ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
++ i+1 < (env->cfg->val_clean_additional?
++ chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
++ chase_reply->rrset_count)) {
++ *suspend = 1;
++ vq->msg_signatures_state = 1;
++ vq->msg_signatures_index = i;
++ verbose(VERB_ALGO, "msg signature validation "
++ "suspended");
++ return 0;
++ }
+ }
+
+ /* If set, the validator should clean the additional section of
+@@ -684,22 +747,102 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
+ /* attempt to validate the ADDITIONAL section rrsets */
+ for(i=chase_reply->an_numrrsets+chase_reply->ns_numrrsets;
+ i<chase_reply->rrset_count; i++) {
++ if(have_state && i <= vq->msg_signatures_index)
++ continue;
+ s = chase_reply->rrsets[i];
+ /* only validate rrs that have signatures with the key */
+ /* leave others unchecked, those get removed later on too */
+ val_find_rrset_signer(s, &sname, &slen);
+
++ verified = 0;
+ if(sname && query_dname_compare(sname, key_entry->name)==0)
+ (void)val_verify_rrset_entry(env, ve, s, key_entry,
+- &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate);
++ &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate,
++ &verified);
+ /* the additional section can fail to be secure,
+ * it is optional, check signature in case we need
+ * to clean the additional section later. */
++ num_verifies += verified;
++ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
++ i+1 < chase_reply->rrset_count) {
++ *suspend = 1;
++ vq->msg_signatures_state = 1;
++ vq->msg_signatures_index = i;
++ verbose(VERB_ALGO, "msg signature validation "
++ "suspended");
++ return 0;
++ }
+ }
+
+ return 1;
+ }
+
++void
++validate_msg_signatures_timer_cb(void* arg)
++{
++ struct module_qstate* qstate = (struct module_qstate*)arg;
++ verbose(VERB_ALGO, "validate_msg_signatures timer, continue");
++ mesh_run(qstate->env->mesh, qstate->mesh_info, module_event_pass,
++ NULL);
++}
++
++/** Setup timer to continue validation of msg signatures later */
++static int
++validate_msg_signatures_setup_timer(struct module_qstate* qstate,
++ struct val_qstate* vq, int id)
++{
++ struct timeval tv;
++ int usec, slack, base;
++ if(vq->suspend_count >= MAX_VALIDATION_SUSPENDS) {
++ verbose(VERB_ALGO, "validate_msg_signatures_setup_timer: "
++ "reached MAX_VALIDATION_SUSPENDS (%d); error out",
++ MAX_VALIDATION_SUSPENDS);
++ errinf(qstate, "max validation suspends reached, "
++ "too many RRSIG validations");
++ return 0;
++ }
++ vq->state = VAL_VALIDATE_STATE;
++ qstate->ext_state[id] = module_wait_reply;
++ if(!vq->msg_signatures_timer) {
++ vq->msg_signatures_timer = comm_timer_create(
++ qstate->env->worker_base,
++ validate_msg_signatures_timer_cb, qstate);
++ if(!vq->msg_signatures_timer) {
++ log_err("validate_msg_signatures_setup_timer: "
++ "out of memory for comm_timer_create");
++ return 0;
++ }
++ }
++ /* The timer is activated later, after other events in the event
++ * loop have been processed. The query state can also be deleted,
++ * when the list is full and query states are dropped. */
++ /* Extend wait time if there are a lot of queries or if this one
++ * is taking long, to keep around cpu time for ordinary queries. */
++ usec = 50000; /* 50 msec */
++ slack = 0;
++ if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states)
++ slack += 3;
++ else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/2)
++ slack += 2;
++ else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/4)
++ slack += 1;
++ if(vq->suspend_count > 3)
++ slack += 3;
++ else if(vq->suspend_count > 0)
++ slack += vq->suspend_count;
++ if(slack != 0 && slack <= 12 /* No numeric overflow. */) {
++ usec = usec << slack;
++ }
++ /* Spread such timeouts within 90%-100% of the original timer. */
++ base = usec * 9/10;
++ usec = base + ub_random_max(qstate->env->rnd, usec-base);
++ tv.tv_usec = (usec % 1000000);
++ tv.tv_sec = (usec / 1000000);
++ vq->suspend_count ++;
++ comm_timer_set(vq->msg_signatures_timer, &tv);
++ return 1;
++}
++
+ /**
+ * Detect wrong truncated response (say from BIND 9.6.1 that is forwarding
+ * and saw the NS record without signatures from a referral).
+@@ -1871,7 +2014,7 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ struct val_env* ve, int id)
+ {
+ enum val_classification subtype;
+- int rcode;
++ int rcode, suspend;
+
+ if(!vq->key_entry) {
+ verbose(VERB_ALGO, "validate: no key entry, failed");
+@@ -1926,8 +2069,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+
+ /* check signatures in the message;
+ * answer and authority must be valid, additional is only checked. */
+- if(!validate_msg_signatures(qstate, qstate->env, ve, &vq->qchase,
+- vq->chase_reply, vq->key_entry)) {
++ if(!validate_msg_signatures(qstate, vq, qstate->env, ve, &vq->qchase,
++ vq->chase_reply, vq->key_entry, &suspend)) {
++ if(suspend) {
++ if(!validate_msg_signatures_setup_timer(qstate, vq,
++ id))
++ return val_error(qstate, id);
++ return 0;
++ }
+ /* workaround bad recursor out there that truncates (even
+ * with EDNS4k) to 512 by removing RRSIG from auth section
+ * for positive replies*/
+@@ -2123,16 +2272,13 @@ processFinished(struct module_qstate* qstate, struct val_qstate* vq,
+ if(vq->orig_msg->rep->security == sec_status_bogus) {
+ /* see if we can try again to fetch data */
+ if(vq->restart_count < ve->max_restart) {
+- int restart_count = vq->restart_count+1;
+ verbose(VERB_ALGO, "validation failed, "
+ "blacklist and retry to fetch data");
+ val_blacklist(&qstate->blacklist, qstate->region,
+ qstate->reply_origin, 0);
+ qstate->reply_origin = NULL;
+ qstate->errinf = NULL;
+- memset(vq, 0, sizeof(*vq));
+- vq->restart_count = restart_count;
+- vq->state = VAL_INIT_STATE;
++ val_restart(vq);
+ verbose(VERB_ALGO, "pass back to next module");
+ qstate->ext_state[id] = module_restart_next;
+ return 0;
+@@ -2451,6 +2597,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ char* reason = NULL;
+ sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+ enum val_classification subtype;
++ int verified;
+ if(rcode != LDNS_RCODE_NOERROR) {
+ char rc[16];
+ rc[0]=0;
+@@ -2479,7 +2626,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ /* Verify only returns BOGUS or SECURE. If the rrset is
+ * bogus, then we are done. */
+ sec = val_verify_rrset_entry(qstate->env, ve, ds,
+- vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate);
++ vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
+ if(sec != sec_status_secure) {
+ verbose(VERB_DETAIL, "DS rrset in DS response did "
+ "not verify");
+@@ -2620,7 +2767,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ goto return_bogus;
+ }
+ sec = val_verify_rrset_entry(qstate->env, ve, cname,
+- vq->key_entry, &reason, NULL, LDNS_SECTION_ANSWER, qstate);
++ vq->key_entry, &reason, NULL, LDNS_SECTION_ANSWER, qstate, &verified);
+ if(sec == sec_status_secure) {
+ verbose(VERB_ALGO, "CNAME validated, "
+ "proof that DS does not exist");
+@@ -2943,8 +3090,15 @@ val_inform_super(struct module_qstate* qstate, int id,
+ void
+ val_clear(struct module_qstate* qstate, int id)
+ {
++ struct val_qstate* vq;
+ if(!qstate)
+ return;
++ vq = (struct val_qstate*)qstate->minfo[id];
++ if(vq) {
++ if(vq->msg_signatures_timer) {
++ comm_timer_delete(vq->msg_signatures_timer);
++ }
++ }
+ /* everything is allocated in the region, so assign NULL */
+ qstate->minfo[id] = NULL;
+ }
+diff --git a/validator/validator.h b/validator/validator.h
+index 694e4c895..a997ca88f 100644
+--- a/validator/validator.h
++++ b/validator/validator.h
+@@ -50,6 +50,7 @@ struct key_cache;
+ struct key_entry_key;
+ struct val_neg_cache;
+ struct config_strlist;
++struct comm_timer;
+
+ /**
+ * This is the TTL to use when a trust anchor fails to prime. A trust anchor
+@@ -215,6 +216,15 @@ struct val_qstate {
+
+ /** true if this state is waiting to prime a trust anchor */
+ int wait_prime_ta;
++
++ /** State to continue with RRSIG validation in a message later */
++ int msg_signatures_state;
++ /** The rrset index for the msg signatures to continue from */
++ size_t msg_signatures_index;
++ /** The timer to resume processing msg signatures */
++ struct comm_timer* msg_signatures_timer;
++ /** number of suspends */
++ int suspend_count;
+ };
+
+ /**
+@@ -262,4 +272,7 @@ void val_clear(struct module_qstate* qstate, int id);
+ */
+ size_t val_get_mem(struct module_env* env, int id);
+
++/** Timer callback for msg signatures continue timer */
++void validate_msg_signatures_timer_cb(void* arg);
++
+ #endif /* VALIDATOR_VALIDATOR_H */
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch unbound-1.17.1/debian/patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
--- unbound-1.17.1/debian/patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch 1970-01-01 03:00:00.000000000 +0300
@@ -1,2299 +0,0 @@
-diff --git a/services/authzone.c b/services/authzone.c
-index 3898767c..4c63b2e0 100644
---- a/services/authzone.c
-+++ b/services/authzone.c
-@@ -7767,6 +7767,7 @@ static int zonemd_dnssec_verify_rrset(struct auth_zone* z,
- enum sec_status sec;
- struct val_env* ve;
- int m;
-+ int verified = 0;
- m = modstack_find(mods, "validator");
- if(m == -1) {
- auth_zone_log(z->name, VERB_ALGO, "zonemd dnssec verify: have "
-@@ -7790,7 +7791,7 @@ static int zonemd_dnssec_verify_rrset(struct auth_zone* z,
- "zonemd: verify %s RRset with DNSKEY", typestr);
- }
- sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg, why_bogus, NULL,
-- LDNS_SECTION_ANSWER, NULL);
-+ LDNS_SECTION_ANSWER, NULL, &verified);
- if(sec == sec_status_secure) {
- return 1;
- }
-diff --git a/services/cache/dns.c b/services/cache/dns.c
-index 6fc9919e..1ced1143 100644
---- a/services/cache/dns.c
-+++ b/services/cache/dns.c
-@@ -703,6 +703,24 @@ tomsg(struct module_env* env, struct query_info* q, struct reply_info* r,
- return msg;
- }
-
-+struct dns_msg*
-+dns_msg_deepcopy_region(struct dns_msg* origin, struct regional* region)
-+{
-+ size_t i;
-+ struct dns_msg* res = NULL;
-+ res = gen_dns_msg(region, &origin->qinfo, origin->rep->rrset_count);
-+ if(!res) return NULL;
-+ *res->rep = *origin->rep;
-+ for(i=0; i<res->rep->rrset_count; i++) {
-+ res->rep->rrsets[i] = packed_rrset_copy_region(
-+ origin->rep->rrsets[i], region, 0);
-+ if(!res->rep->rrsets[i]) {
-+ return NULL;
-+ }
-+ }
-+ return res;
-+}
-+
- /** synthesize RRset-only response from cached RRset item */
- static struct dns_msg*
- rrset_msg(struct ub_packed_rrset_key* rrset, struct regional* region,
-diff --git a/services/cache/dns.h b/services/cache/dns.h
-index 147f992c..c2bf23c6 100644
---- a/services/cache/dns.h
-+++ b/services/cache/dns.h
-@@ -164,6 +164,15 @@ struct dns_msg* tomsg(struct module_env* env, struct query_info* q,
- struct reply_info* r, struct regional* region, time_t now,
- int allow_expired, struct regional* scratch);
-
-+/**
-+ * Deep copy a dns_msg to a region.
-+ * @param origin: the dns_msg to copy.
-+ * @param region: the region to copy all the data to.
-+ * @return the new dns_msg or NULL on malloc error.
-+ */
-+struct dns_msg* dns_msg_deepcopy_region(struct dns_msg* origin,
-+ struct regional* region);
-+
- /**
- * Find cached message
- * @param env: module environment with the DNS cache.
-diff --git a/testcode/unitverify.c b/testcode/unitverify.c
-index ff069a1b..395b4c25 100644
---- a/testcode/unitverify.c
-+++ b/testcode/unitverify.c
-@@ -180,6 +180,7 @@ verifytest_rrset(struct module_env* env, struct val_env* ve,
- enum sec_status sec;
- char* reason = NULL;
- uint8_t sigalg[ALGO_NEEDS_MAX+1];
-+ int verified = 0;
- if(vsig) {
- log_nametypeclass(VERB_QUERY, "verify of rrset",
- rrset->rk.dname, ntohs(rrset->rk.type),
-@@ -188,7 +189,7 @@ verifytest_rrset(struct module_env* env, struct val_env* ve,
- setup_sigalg(dnskey, sigalg); /* check all algorithms in the dnskey */
- /* ok to give null as qstate here, won't be used for answer section. */
- sec = dnskeyset_verify_rrset(env, ve, rrset, dnskey, sigalg, &reason, NULL,
-- LDNS_SECTION_ANSWER, NULL);
-+ LDNS_SECTION_ANSWER, NULL, &verified);
- if(vsig) {
- printf("verify outcome is: %s %s\n", sec_status_to_string(sec),
- reason?reason:"");
-@@ -442,9 +443,9 @@ nsec3_hash_test_entry(struct entry* e, rbtree_type* ct,
-
- ret = nsec3_hash_name(ct, region, buf, nsec3, 0, qname,
- qinfo.qname_len, &hash);
-- if(ret != 1) {
-+ if(ret < 1) {
- printf("Bad nsec3_hash_name retcode %d\n", ret);
-- unit_assert(ret == 1);
-+ unit_assert(ret == 1 || ret == 2);
- }
- unit_assert(hash->dname && hash->hash && hash->hash_len &&
- hash->b32 && hash->b32_len);
-diff --git a/testdata/val_any.rpl b/testdata/val_any.rpl
-index 4ce19513..90263af8 100644
---- a/testdata/val_any.rpl
-+++ b/testdata/val_any.rpl
-@@ -161,6 +161,9 @@ SECTION QUESTION
- example.com. IN ANY
- ENTRY_END
-
-+; Allow validation resuming for the RRSIGs
-+STEP 2 TIME_PASSES ELAPSE 0.05
-+
- ; recursion happens here.
- STEP 10 CHECK_ANSWER
- ENTRY_BEGIN
-diff --git a/testdata/val_any_dname.rpl b/testdata/val_any_dname.rpl
-index 6ab3cded..dd65e97b 100644
---- a/testdata/val_any_dname.rpl
-+++ b/testdata/val_any_dname.rpl
-@@ -163,6 +163,9 @@ SECTION QUESTION
- example.com. IN ANY
- ENTRY_END
-
-+; Allow validation resuming for the RRSIGs
-+STEP 2 TIME_PASSES ELAPSE 0.05
-+
- ; recursion happens here.
- STEP 10 CHECK_ANSWER
- ENTRY_BEGIN
-diff --git a/testdata/val_nx_nsec3_collision.rpl b/testdata/val_nx_nsec3_collision.rpl
-index 8ff7e4b0..87a55f56 100644
---- a/testdata/val_nx_nsec3_collision.rpl
-+++ b/testdata/val_nx_nsec3_collision.rpl
-@@ -156,6 +156,9 @@ SECTION QUESTION
- www.example.com. IN A
- ENTRY_END
-
-+; Allow validation resuming for NSEC3 hash calculations
-+STEP 2 TIME_PASSES ELAPSE 0.05
-+
- ; recursion happens here.
- STEP 10 CHECK_ANSWER
- ENTRY_BEGIN
-diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c
-index dc8ab669..00b73253 100644
---- a/util/fptr_wlist.c
-+++ b/util/fptr_wlist.c
-@@ -131,6 +131,7 @@ fptr_whitelist_comm_timer(void (*fptr)(void*))
- else if(fptr == &pending_udp_timer_delay_cb) return 1;
- else if(fptr == &worker_stat_timer_cb) return 1;
- else if(fptr == &worker_probe_timer_cb) return 1;
-+ else if(fptr == &validate_suspend_timer_cb) return 1;
- #ifdef UB_ON_WINDOWS
- else if(fptr == &wsvc_cron_cb) return 1;
- #endif
-diff --git a/validator/val_nsec.c b/validator/val_nsec.c
-index 876bfab6..5871db90 100644
---- a/validator/val_nsec.c
-+++ b/validator/val_nsec.c
-@@ -180,6 +180,7 @@ nsec_verify_rrset(struct module_env* env, struct val_env* ve,
- {
- struct packed_rrset_data* d = (struct packed_rrset_data*)
- nsec->entry.data;
-+ int verified = 0;
- if(!d) return 0;
- if(d->security == sec_status_secure)
- return 1;
-@@ -187,7 +188,7 @@ nsec_verify_rrset(struct module_env* env, struct val_env* ve,
- if(d->security == sec_status_secure)
- return 1;
- d->security = val_verify_rrset_entry(env, ve, nsec, kkey, reason,
-- NULL, LDNS_SECTION_AUTHORITY, qstate);
-+ NULL, LDNS_SECTION_AUTHORITY, qstate, &verified);
- if(d->security == sec_status_secure) {
- rrset_update_sec_status(env->rrset_cache, nsec, *env->now);
- return 1;
-diff --git a/validator/val_nsec3.c b/validator/val_nsec3.c
-index a2b3794f..95d1e4d7 100644
---- a/validator/val_nsec3.c
-+++ b/validator/val_nsec3.c
-@@ -57,6 +57,19 @@
- /* we include nsec.h for the bitmap_has_type function */
- #include "validator/val_nsec.h"
- #include "sldns/sbuffer.h"
-+#include "util/config_file.h"
-+
-+/**
-+ * Max number of NSEC3 calculations at once, suspend query for later.
-+ * 8 is low enough and allows for cases where multiple proofs are needed.
-+ */
-+#define MAX_NSEC3_CALCULATIONS 8
-+/**
-+ * When all allowed NSEC3 calculations at once resulted in error treat as
-+ * bogus. NSEC3 hash errors are not cached and this helps breaks loops with
-+ * erroneous data.
-+ */
-+#define MAX_NSEC3_ERRORS -1
-
- /**
- * This function we get from ldns-compat or from base system
-@@ -532,6 +545,17 @@ nsec3_hash_cmp(const void* c1, const void* c2)
- return memcmp(s1, s2, s1len);
- }
-
-+int
-+nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region)
-+{
-+ if(ct->ct) return 1;
-+ ct->ct = (rbtree_type*)regional_alloc(region, sizeof(*ct->ct));
-+ if(!ct->ct) return 0;
-+ ct->region = region;
-+ rbtree_init(ct->ct, &nsec3_hash_cmp);
-+ return 1;
-+}
-+
- size_t
- nsec3_get_hashed(sldns_buffer* buf, uint8_t* nm, size_t nmlen, int algo,
- size_t iter, uint8_t* salt, size_t saltlen, uint8_t* res, size_t max)
-@@ -646,7 +670,7 @@ nsec3_hash_name(rbtree_type* table, struct regional* region, sldns_buffer* buf,
- c = (struct nsec3_cached_hash*)rbtree_search(table, &looki);
- if(c) {
- *hash = c;
-- return 1;
-+ return 2;
- }
- /* create a new entry */
- c = (struct nsec3_cached_hash*)regional_alloc(region, sizeof(*c));
-@@ -658,10 +682,10 @@ nsec3_hash_name(rbtree_type* table, struct regional* region, sldns_buffer* buf,
- c->dname_len = dname_len;
- r = nsec3_calc_hash(region, buf, c);
- if(r != 1)
-- return r;
-+ return r; /* returns -1 or 0 */
- r = nsec3_calc_b32(region, buf, c);
- if(r != 1)
-- return r;
-+ return r; /* returns 0 */
- #ifdef UNBOUND_DEBUG
- n =
- #else
-@@ -704,6 +728,7 @@ nsec3_hash_matches_owner(struct nsec3_filter* flt,
- struct nsec3_cached_hash* hash, struct ub_packed_rrset_key* s)
- {
- uint8_t* nm = s->rk.dname;
-+ if(!hash) return 0; /* please clang */
- /* compare, does hash of name based on params in this NSEC3
- * match the owner name of this NSEC3?
- * name must be: <hashlength>base32 . zone name
-@@ -730,34 +755,50 @@ nsec3_hash_matches_owner(struct nsec3_filter* flt,
- * @param nmlen: length of name.
- * @param rrset: nsec3 that matches is returned here.
- * @param rr: rr number in nsec3 rrset that matches.
-+ * @param calculations: current hash calculations.
- * @return true if a matching NSEC3 is found, false if not.
- */
- static int
- find_matching_nsec3(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, uint8_t* nm, size_t nmlen,
-- struct ub_packed_rrset_key** rrset, int* rr)
-+ struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
-+ struct ub_packed_rrset_key** rrset, int* rr,
-+ int* calculations)
- {
- size_t i_rs;
- int i_rr;
- struct ub_packed_rrset_key* s;
- struct nsec3_cached_hash* hash = NULL;
- int r;
-+ int calc_errors = 0;
-
- /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
- for(s=filter_first(flt, &i_rs, &i_rr); s;
- s=filter_next(flt, &i_rs, &i_rr)) {
-+ /* check if we are allowed more calculations */
-+ if(*calculations >= MAX_NSEC3_CALCULATIONS) {
-+ if(calc_errors == *calculations) {
-+ *calculations = MAX_NSEC3_ERRORS;
-+ }
-+ break;
-+ }
- /* get name hashed for this NSEC3 RR */
-- r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
-+ r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
- s, i_rr, nm, nmlen, &hash);
- if(r == 0) {
- log_err("nsec3: malloc failure");
- break; /* alloc failure */
-- } else if(r != 1)
-- continue; /* malformed NSEC3 */
-- else if(nsec3_hash_matches_owner(flt, hash, s)) {
-- *rrset = s; /* rrset with this name */
-- *rr = i_rr; /* matches hash with these parameters */
-- return 1;
-+ } else if(r < 0) {
-+ /* malformed NSEC3 */
-+ calc_errors++;
-+ (*calculations)++;
-+ continue;
-+ } else {
-+ if(r == 1) (*calculations)++;
-+ if(nsec3_hash_matches_owner(flt, hash, s)) {
-+ *rrset = s; /* rrset with this name */
-+ *rr = i_rr; /* matches hash with these parameters */
-+ return 1;
-+ }
- }
- }
- *rrset = NULL;
-@@ -775,6 +816,7 @@ nsec3_covers(uint8_t* zone, struct nsec3_cached_hash* hash,
- if(!nsec3_get_nextowner(rrset, rr, &next, &nextlen))
- return 0; /* malformed RR proves nothing */
-
-+ if(!hash) return 0; /* please clang */
- /* check the owner name is a hashed value . apex
- * base32 encoded values must have equal length.
- * hash_value and next hash value must have equal length. */
-@@ -823,35 +865,51 @@ nsec3_covers(uint8_t* zone, struct nsec3_cached_hash* hash,
- * @param nmlen: length of name.
- * @param rrset: covering NSEC3 rrset is returned here.
- * @param rr: rr of cover is returned here.
-+ * @param calculations: current hash calculations.
- * @return true if a covering NSEC3 is found, false if not.
- */
- static int
- find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, uint8_t* nm, size_t nmlen,
-- struct ub_packed_rrset_key** rrset, int* rr)
-+ struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
-+ struct ub_packed_rrset_key** rrset, int* rr,
-+ int* calculations)
- {
- size_t i_rs;
- int i_rr;
- struct ub_packed_rrset_key* s;
- struct nsec3_cached_hash* hash = NULL;
- int r;
-+ int calc_errors = 0;
-
- /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
- for(s=filter_first(flt, &i_rs, &i_rr); s;
- s=filter_next(flt, &i_rs, &i_rr)) {
-+ /* check if we are allowed more calculations */
-+ if(*calculations >= MAX_NSEC3_CALCULATIONS) {
-+ if(calc_errors == *calculations) {
-+ *calculations = MAX_NSEC3_ERRORS;
-+ }
-+ break;
-+ }
- /* get name hashed for this NSEC3 RR */
-- r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
-+ r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
- s, i_rr, nm, nmlen, &hash);
- if(r == 0) {
- log_err("nsec3: malloc failure");
- break; /* alloc failure */
-- } else if(r != 1)
-- continue; /* malformed NSEC3 */
-- else if(nsec3_covers(flt->zone, hash, s, i_rr,
-- env->scratch_buffer)) {
-- *rrset = s; /* rrset with this name */
-- *rr = i_rr; /* covers hash with these parameters */
-- return 1;
-+ } else if(r < 0) {
-+ /* malformed NSEC3 */
-+ calc_errors++;
-+ (*calculations)++;
-+ continue;
-+ } else {
-+ if(r == 1) (*calculations)++;
-+ if(nsec3_covers(flt->zone, hash, s, i_rr,
-+ env->scratch_buffer)) {
-+ *rrset = s; /* rrset with this name */
-+ *rr = i_rr; /* covers hash with these parameters */
-+ return 1;
-+ }
- }
- }
- *rrset = NULL;
-@@ -869,11 +927,13 @@ find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
- * @param ct: cached hashes table.
- * @param qinfo: query that is verified for.
- * @param ce: closest encloser information is returned in here.
-+ * @param calculations: current hash calculations.
- * @return true if a closest encloser candidate is found, false if not.
- */
- static int
--nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, struct query_info* qinfo, struct ce_response* ce)
-+nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-+ struct nsec3_cache_table* ct, struct query_info* qinfo,
-+ struct ce_response* ce, int* calculations)
- {
- uint8_t* nm = qinfo->qname;
- size_t nmlen = qinfo->qname_len;
-@@ -888,8 +948,12 @@ nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
- * may be the case. */
-
- while(dname_subdomain_c(nm, flt->zone)) {
-+ if(*calculations >= MAX_NSEC3_CALCULATIONS ||
-+ *calculations == MAX_NSEC3_ERRORS) {
-+ return 0;
-+ }
- if(find_matching_nsec3(env, flt, ct, nm, nmlen,
-- &ce->ce_rrset, &ce->ce_rr)) {
-+ &ce->ce_rrset, &ce->ce_rr, calculations)) {
- ce->ce = nm;
- ce->ce_len = nmlen;
- return 1;
-@@ -933,22 +997,38 @@ next_closer(uint8_t* qname, size_t qnamelen, uint8_t* ce,
- * If set true, and the return value is true, then you can be
- * certain that the ce.nc_rrset and ce.nc_rr are set properly.
- * @param ce: closest encloser information is returned in here.
-+ * @param calculations: pointer to the current NSEC3 hash calculations.
- * @return bogus if no closest encloser could be proven.
- * secure if a closest encloser could be proven, ce is set.
- * insecure if the closest-encloser candidate turns out to prove
- * that an insecure delegation exists above the qname.
-+ * unchecked if no more hash calculations are allowed at this point.
- */
- static enum sec_status
--nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, struct query_info* qinfo, int prove_does_not_exist,
-- struct ce_response* ce)
-+nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-+ struct nsec3_cache_table* ct, struct query_info* qinfo,
-+ int prove_does_not_exist, struct ce_response* ce, int* calculations)
- {
- uint8_t* nc;
- size_t nc_len;
- /* robust: clean out ce, in case it gets abused later */
- memset(ce, 0, sizeof(*ce));
-
-- if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce)) {
-+ if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce, calculations)) {
-+ if(*calculations == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
-+ "not find a candidate for the closest "
-+ "encloser; all attempted hash calculations "
-+ "were erroneous; bogus");
-+ return sec_status_bogus;
-+ } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
-+ "not find a candidate for the closest "
-+ "encloser; reached MAX_NSEC3_CALCULATIONS "
-+ "(%d); unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
- verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
- "not find a candidate for the closest encloser.");
- return sec_status_bogus;
-@@ -989,9 +1069,23 @@ nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
- /* Otherwise, we need to show that the next closer name is covered. */
- next_closer(qinfo->qname, qinfo->qname_len, ce->ce, &nc, &nc_len);
- if(!find_covering_nsec3(env, flt, ct, nc, nc_len,
-- &ce->nc_rrset, &ce->nc_rr)) {
-+ &ce->nc_rrset, &ce->nc_rr, calculations)) {
-+ if(*calculations == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "nsec3: Could not find proof that the "
-+ "candidate encloser was the closest encloser; "
-+ "all attempted hash calculations were "
-+ "erroneous; bogus");
-+ return sec_status_bogus;
-+ } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "nsec3: Could not find proof that the "
-+ "candidate encloser was the closest encloser; "
-+ "reached MAX_NSEC3_CALCULATIONS (%d); "
-+ "unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
- verbose(VERB_ALGO, "nsec3: Could not find proof that the "
-- "candidate encloser was the closest encloser");
-+ "candidate encloser was the closest encloser");
- return sec_status_bogus;
- }
- return sec_status_secure;
-@@ -1019,8 +1113,8 @@ nsec3_ce_wildcard(struct regional* region, uint8_t* ce, size_t celen,
-
- /** Do the name error proof */
- static enum sec_status
--nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, struct query_info* qinfo)
-+nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
-+ struct nsec3_cache_table* ct, struct query_info* qinfo, int* calc)
- {
- struct ce_response ce;
- uint8_t* wc;
-@@ -1032,11 +1126,15 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
- /* First locate and prove the closest encloser to qname. We will
- * use the variant that fails if the closest encloser turns out
- * to be qname. */
-- sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
-+ sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
- if(sec != sec_status_secure) {
- if(sec == sec_status_bogus)
- verbose(VERB_ALGO, "nsec3 nameerror proof: failed "
- "to prove a closest encloser");
-+ else if(sec == sec_status_unchecked)
-+ verbose(VERB_ALGO, "nsec3 nameerror proof: will "
-+ "continue proving closest encloser after "
-+ "suspend");
- else verbose(VERB_ALGO, "nsec3 nameerror proof: closest "
- "nsec3 is an insecure delegation");
- return sec;
-@@ -1046,9 +1144,27 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
- /* At this point, we know that qname does not exist. Now we need
- * to prove that the wildcard does not exist. */
- log_assert(ce.ce);
-- wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-- if(!wc || !find_covering_nsec3(env, flt, ct, wc, wclen,
-- &wc_rrset, &wc_rr)) {
-+ wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
-+ if(!wc) {
-+ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
-+ "that the applicable wildcard did not exist.");
-+ return sec_status_bogus;
-+ }
-+ if(!find_covering_nsec3(env, flt, ct, wc, wclen, &wc_rrset, &wc_rr, calc)) {
-+ if(*calc == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
-+ "that the applicable wildcard did not exist; "
-+ "all attempted hash calculations were "
-+ "erroneous; bogus");
-+ return sec_status_bogus;
-+ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
-+ "that the applicable wildcard did not exist; "
-+ "reached MAX_NSEC3_CALCULATIONS (%d); "
-+ "unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
- verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
- "that the applicable wildcard did not exist.");
- return sec_status_bogus;
-@@ -1064,14 +1180,13 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
- enum sec_status
- nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey)
-+ struct query_info* qinfo, struct key_entry_key* kkey,
-+ struct nsec3_cache_table* ct, int* calc)
- {
-- rbtree_type ct;
- struct nsec3_filter flt;
-
- if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
- return sec_status_bogus; /* no valid NSEC3s, bogus */
-- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
- filter_init(&flt, list, num, qinfo); /* init RR iterator */
- if(!flt.zone)
- return sec_status_bogus; /* no RRs */
-@@ -1079,7 +1194,7 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
- return sec_status_insecure; /* iteration count too high */
- log_nametypeclass(VERB_ALGO, "start nsec3 nameerror proof, zone",
- flt.zone, 0, 0);
-- return nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
-+ return nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
- }
-
- /*
-@@ -1089,8 +1204,9 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
-
- /** Do the nodata proof */
- static enum sec_status
--nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
-- rbtree_type* ct, struct query_info* qinfo)
-+nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
-+ struct nsec3_cache_table* ct, struct query_info* qinfo,
-+ int* calc)
- {
- struct ce_response ce;
- uint8_t* wc;
-@@ -1100,7 +1216,7 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
- enum sec_status sec;
-
- if(find_matching_nsec3(env, flt, ct, qinfo->qname, qinfo->qname_len,
-- &rrset, &rr)) {
-+ &rrset, &rr, calc)) {
- /* cases 1 and 2 */
- if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
- verbose(VERB_ALGO, "proveNodata: Matching NSEC3 "
-@@ -1144,11 +1260,23 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
- }
- return sec_status_secure;
- }
-+ if(*calc == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "proveNodata: all attempted hash "
-+ "calculations were erroneous while finding a matching "
-+ "NSEC3, bogus");
-+ return sec_status_bogus;
-+ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "proveNodata: reached "
-+ "MAX_NSEC3_CALCULATIONS (%d) while finding a "
-+ "matching NSEC3; unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
-
- /* For cases 3 - 5, we need the proven closest encloser, and it
- * can't match qname. Although, at this point, we know that it
- * won't since we just checked that. */
-- sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
-+ sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
- if(sec == sec_status_bogus) {
- verbose(VERB_ALGO, "proveNodata: did not match qname, "
- "nor found a proven closest encloser.");
-@@ -1157,14 +1285,17 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
- verbose(VERB_ALGO, "proveNodata: closest nsec3 is insecure "
- "delegation.");
- return sec_status_insecure;
-+ } else if(sec==sec_status_unchecked) {
-+ return sec_status_unchecked;
- }
-
- /* Case 3: removed */
-
- /* Case 4: */
- log_assert(ce.ce);
-- wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-- if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr)) {
-+ wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
-+ if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr,
-+ calc)) {
- /* found wildcard */
- if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
- verbose(VERB_ALGO, "nsec3 nodata proof: matching "
-@@ -1195,6 +1326,18 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
- }
- return sec_status_secure;
- }
-+ if(*calc == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "nsec3 nodata proof: all attempted hash "
-+ "calculations were erroneous while matching "
-+ "wildcard, bogus");
-+ return sec_status_bogus;
-+ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "nsec3 nodata proof: reached "
-+ "MAX_NSEC3_CALCULATIONS (%d) while matching "
-+ "wildcard, unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
-
- /* Case 5: */
- /* Due to forwarders, cnames, and other collating effects, we
-@@ -1223,28 +1366,27 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
- enum sec_status
- nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey)
-+ struct query_info* qinfo, struct key_entry_key* kkey,
-+ struct nsec3_cache_table* ct, int* calc)
- {
-- rbtree_type ct;
- struct nsec3_filter flt;
-
- if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
- return sec_status_bogus; /* no valid NSEC3s, bogus */
-- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
- filter_init(&flt, list, num, qinfo); /* init RR iterator */
- if(!flt.zone)
- return sec_status_bogus; /* no RRs */
- if(nsec3_iteration_count_high(ve, &flt, kkey))
- return sec_status_insecure; /* iteration count too high */
-- return nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
-+ return nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
- }
-
- enum sec_status
- nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc)
-+ struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
-+ struct nsec3_cache_table* ct, int* calc)
- {
-- rbtree_type ct;
- struct nsec3_filter flt;
- struct ce_response ce;
- uint8_t* nc;
-@@ -1254,7 +1396,6 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
-
- if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
- return sec_status_bogus; /* no valid NSEC3s, bogus */
-- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
- filter_init(&flt, list, num, qinfo); /* init RR iterator */
- if(!flt.zone)
- return sec_status_bogus; /* no RRs */
-@@ -1272,8 +1413,22 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
- /* Now we still need to prove that the original data did not exist.
- * Otherwise, we need to show that the next closer name is covered. */
- next_closer(qinfo->qname, qinfo->qname_len, ce.ce, &nc, &nc_len);
-- if(!find_covering_nsec3(env, &flt, &ct, nc, nc_len,
-- &ce.nc_rrset, &ce.nc_rr)) {
-+ if(!find_covering_nsec3(env, &flt, ct, nc, nc_len,
-+ &ce.nc_rrset, &ce.nc_rr, calc)) {
-+ if(*calc == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "proveWildcard: did not find a "
-+ "covering NSEC3 that covered the next closer "
-+ "name; all attempted hash calculations were "
-+ "erroneous; bogus");
-+ return sec_status_bogus;
-+ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "proveWildcard: did not find a "
-+ "covering NSEC3 that covered the next closer "
-+ "name; reached MAX_NSEC3_CALCULATIONS "
-+ "(%d); unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
- verbose(VERB_ALGO, "proveWildcard: did not find a covering "
- "NSEC3 that covered the next closer name.");
- return sec_status_bogus;
-@@ -1294,6 +1449,7 @@ list_is_secure(struct module_env* env, struct val_env* ve,
- {
- struct packed_rrset_data* d;
- size_t i;
-+ int verified = 0;
- for(i=0; i<num; i++) {
- d = (struct packed_rrset_data*)list[i]->entry.data;
- if(list[i]->rk.type != htons(LDNS_RR_TYPE_NSEC3))
-@@ -1304,7 +1460,8 @@ list_is_secure(struct module_env* env, struct val_env* ve,
- if(d->security == sec_status_secure)
- continue;
- d->security = val_verify_rrset_entry(env, ve, list[i], kkey,
-- reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
-+ reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
-+ &verified);
- if(d->security != sec_status_secure) {
- verbose(VERB_ALGO, "NSEC3 did not verify");
- return 0;
-@@ -1318,13 +1475,16 @@ enum sec_status
- nsec3_prove_nods(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
- struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-- sldns_ede_code* reason_bogus, struct module_qstate* qstate)
-+ sldns_ede_code* reason_bogus, struct module_qstate* qstate,
-+ struct nsec3_cache_table* ct)
- {
-- rbtree_type ct;
- struct nsec3_filter flt;
- struct ce_response ce;
- struct ub_packed_rrset_key* rrset;
- int rr;
-+ int calc = 0;
-+ enum sec_status sec;
-+
- log_assert(qinfo->qtype == LDNS_RR_TYPE_DS);
-
- if(!list || num == 0 || !kkey || !key_entry_isgood(kkey)) {
-@@ -1335,7 +1495,6 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
- *reason = "not all NSEC3 records secure";
- return sec_status_bogus; /* not all NSEC3 records secure */
- }
-- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
- filter_init(&flt, list, num, qinfo); /* init RR iterator */
- if(!flt.zone) {
- *reason = "no NSEC3 records";
-@@ -1346,8 +1505,8 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
-
- /* Look for a matching NSEC3 to qname -- this is the normal
- * NODATA case. */
-- if(find_matching_nsec3(env, &flt, &ct, qinfo->qname, qinfo->qname_len,
-- &rrset, &rr)) {
-+ if(find_matching_nsec3(env, &flt, ct, qinfo->qname, qinfo->qname_len,
-+ &rrset, &rr, &calc)) {
- /* If the matching NSEC3 has the SOA bit set, it is from
- * the wrong zone (the child instead of the parent). If
- * it has the DS bit set, then we were lied to. */
-@@ -1370,10 +1529,24 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
- /* Otherwise, this proves no DS. */
- return sec_status_secure;
- }
-+ if(calc == MAX_NSEC3_ERRORS) {
-+ verbose(VERB_ALGO, "nsec3 provenods: all attempted hash "
-+ "calculations were erroneous while finding a matching "
-+ "NSEC3, bogus");
-+ return sec_status_bogus;
-+ } else if(calc >= MAX_NSEC3_CALCULATIONS) {
-+ verbose(VERB_ALGO, "nsec3 provenods: reached "
-+ "MAX_NSEC3_CALCULATIONS (%d) while finding a "
-+ "matching NSEC3, unchecked still",
-+ MAX_NSEC3_CALCULATIONS);
-+ return sec_status_unchecked;
-+ }
-
- /* Otherwise, we are probably in the opt-out case. */
-- if(nsec3_prove_closest_encloser(env, &flt, &ct, qinfo, 1, &ce)
-- != sec_status_secure) {
-+ sec = nsec3_prove_closest_encloser(env, &flt, ct, qinfo, 1, &ce, &calc);
-+ if(sec == sec_status_unchecked) {
-+ return sec_status_unchecked;
-+ } else if(sec != sec_status_secure) {
- /* an insecure delegation *above* the qname does not prove
- * anything about this qname exactly, and bogus is bogus */
- verbose(VERB_ALGO, "nsec3 provenods: did not match qname, "
-@@ -1407,17 +1580,16 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
-
- enum sec_status
- nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
-- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey, int* nodata)
-+ struct ub_packed_rrset_key** list, size_t num,
-+ struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
-+ struct nsec3_cache_table* ct, int* calc)
- {
- enum sec_status sec, secnx;
-- rbtree_type ct;
- struct nsec3_filter flt;
- *nodata = 0;
-
- if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
- return sec_status_bogus; /* no valid NSEC3s, bogus */
-- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
- filter_init(&flt, list, num, qinfo); /* init RR iterator */
- if(!flt.zone)
- return sec_status_bogus; /* no RRs */
-@@ -1427,16 +1599,20 @@ nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
- /* try nxdomain and nodata after another, while keeping the
- * hash cache intact */
-
-- secnx = nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
-+ secnx = nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
- if(secnx==sec_status_secure)
- return sec_status_secure;
-- sec = nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
-+ else if(secnx == sec_status_unchecked)
-+ return sec_status_unchecked;
-+ sec = nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
- if(sec==sec_status_secure) {
- *nodata = 1;
- } else if(sec == sec_status_insecure) {
- *nodata = 1;
- } else if(secnx == sec_status_insecure) {
- sec = sec_status_insecure;
-+ } else if(sec == sec_status_unchecked) {
-+ return sec_status_unchecked;
- }
- return sec;
- }
-diff --git a/validator/val_nsec3.h b/validator/val_nsec3.h
-index 7676fc8b..8ca91293 100644
---- a/validator/val_nsec3.h
-+++ b/validator/val_nsec3.h
-@@ -98,6 +98,15 @@ struct sldns_buffer;
- /** The SHA1 hash algorithm for NSEC3 */
- #define NSEC3_HASH_SHA1 0x01
-
-+/**
-+* Cache table for NSEC3 hashes.
-+* It keeps a *pointer* to the region its items are allocated.
-+*/
-+struct nsec3_cache_table {
-+ rbtree_type* ct;
-+ struct regional* region;
-+};
-+
- /**
- * Determine if the set of NSEC3 records provided with a response prove NAME
- * ERROR. This means that the NSEC3s prove a) the closest encloser exists,
-@@ -110,14 +119,18 @@ struct sldns_buffer;
- * @param num: number of RRsets in the array to examine.
- * @param qinfo: query that is verified for.
- * @param kkey: key entry that signed the NSEC3s.
-+ * @param ct: cached hashes table.
-+ * @param calc: current hash calculations.
- * @return:
- * sec_status SECURE of the Name Error is proven by the NSEC3 RRs,
-- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
-+ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
-+ * UNCHECKED if no more hash calculations are allowed at this point.
- */
- enum sec_status
- nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey);
-+ struct query_info* qinfo, struct key_entry_key* kkey,
-+ struct nsec3_cache_table* ct, int* calc);
-
- /**
- * Determine if the NSEC3s provided in a response prove the NOERROR/NODATA
-@@ -144,15 +157,18 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
- * @param num: number of RRsets in the array to examine.
- * @param qinfo: query that is verified for.
- * @param kkey: key entry that signed the NSEC3s.
-+ * @param ct: cached hashes table.
-+ * @param calc: current hash calculations.
- * @return:
- * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
-- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
-+ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
-+ * UNCHECKED if no more hash calculations are allowed at this point.
- */
- enum sec_status
- nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey);
--
-+ struct query_info* qinfo, struct key_entry_key* kkey,
-+ struct nsec3_cache_table* ct, int* calc);
-
- /**
- * Prove that a positive wildcard match was appropriate (no direct match
-@@ -166,14 +182,18 @@ nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
- * @param kkey: key entry that signed the NSEC3s.
- * @param wc: The purported wildcard that matched. This is the wildcard name
- * as *.wildcard.name., with the *. label already removed.
-+ * @param ct: cached hashes table.
-+ * @param calc: current hash calculations.
- * @return:
- * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
-- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
-+ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
-+ * UNCHECKED if no more hash calculations are allowed at this point.
- */
- enum sec_status
- nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc);
-+ struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
-+ struct nsec3_cache_table* ct, int* calc);
-
- /**
- * Prove that a DS response either had no DS, or wasn't a delegation point.
-@@ -189,17 +209,20 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
- * @param reason: string for bogus result.
- * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
- * @param qstate: qstate with region.
-+ * @param ct: cached hashes table.
- * @return:
- * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
- * or if there was no DS in an insecure (i.e., opt-in) way,
-- * INDETERMINATE if it was clear that this wasn't a delegation point.
-+ * INDETERMINATE if it was clear that this wasn't a delegation point,
-+ * UNCHECKED if no more hash calculations are allowed at this point.
- */
- enum sec_status
- nsec3_prove_nods(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
- struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-- sldns_ede_code* reason_bogus, struct module_qstate* qstate);
-+ sldns_ede_code* reason_bogus, struct module_qstate* qstate,
-+ struct nsec3_cache_table* ct);
-
- /**
- * Prove NXDOMAIN or NODATA.
-@@ -212,14 +235,18 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
- * @param kkey: key entry that signed the NSEC3s.
- * @param nodata: if return value is secure, this indicates if nodata or
- * nxdomain was proven.
-+ * @param ct: cached hashes table.
-+ * @param calc: current hash calculations.
- * @return:
- * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
-- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
-+ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
-+ * UNCHECKED if no more hash calculations are allowed at this point.
- */
- enum sec_status
- nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key** list, size_t num,
-- struct query_info* qinfo, struct key_entry_key* kkey, int* nodata);
-+ struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
-+ struct nsec3_cache_table* ct, int* calc);
-
- /**
- * The NSEC3 hash result storage.
-@@ -256,6 +283,14 @@ struct nsec3_cached_hash {
- */
- int nsec3_hash_cmp(const void* c1, const void* c2);
-
-+/**
-+ * Initialise the NSEC3 cache table.
-+ * @param ct: the nsec3 cache table.
-+ * @param region: the region where allocations for the table will happen.
-+ * @return true on success, false on malloc error.
-+ */
-+int nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region);
-+
- /**
- * Obtain the hash of an owner name.
- * Used internally by the nsec3 proof functions in this file.
-@@ -272,7 +307,8 @@ int nsec3_hash_cmp(const void* c1, const void* c2);
- * @param dname_len: the length of the name.
- * @param hash: the hash node is returned on success.
- * @return:
-- * 1 on success, either from cache or newly hashed hash is returned.
-+ * 2 on success, hash from cache is returned.
-+ * 1 on success, newly computed hash is returned.
- * 0 on a malloc failure.
- * -1 if the NSEC3 rr was badly formatted (i.e. formerr).
- */
-diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c
-index 5ab21e20..8600a682 100644
---- a/validator/val_sigcrypt.c
-+++ b/validator/val_sigcrypt.c
-@@ -78,6 +78,9 @@
- #include <openssl/engine.h>
- #endif
-
-+/** Maximum number of RRSIG validations for an RRset. */
-+#define MAX_VALIDATE_RRSIGS 8
-+
- /** return number of rrs in an rrset */
- static size_t
- rrset_get_count(struct ub_packed_rrset_key* rrset)
-@@ -541,6 +544,8 @@ int algo_needs_missing(struct algo_needs* n)
- * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
- * @param section: section of packet where this rrset comes from.
- * @param qstate: qstate with region.
-+ * @param numverified: incremented when the number of RRSIG validations
-+ * increases.
- * @return secure if any key signs *this* signature. bogus if no key signs it,
- * unchecked on error, or indeterminate if all keys are not supported by
- * the crypto library (openssl3+ only).
-@@ -551,7 +556,8 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key* dnskey, size_t sig_idx,
- struct rbtree_type** sortree,
- char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate)
-+ sldns_pkt_section section, struct module_qstate* qstate,
-+ int* numverified)
- {
- /* find matching keys and check them */
- enum sec_status sec = sec_status_bogus;
-@@ -575,6 +581,7 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
- tag != dnskey_calc_keytag(dnskey, i))
- continue;
- numchecked ++;
-+ (*numverified)++;
-
- /* see if key verifies */
- sec = dnskey_verify_rrset_sig(env->scratch,
-@@ -585,6 +592,13 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve,
- return sec;
- else if(sec == sec_status_indeterminate)
- numindeterminate ++;
-+ if(*numverified > MAX_VALIDATE_RRSIGS) {
-+ *reason = "too many RRSIG validations";
-+ if(reason_bogus)
-+ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
-+ verbose(VERB_ALGO, "verify sig: too many RRSIG validations");
-+ return sec_status_bogus;
-+ }
- }
- if(numchecked == 0) {
- *reason = "signatures from unknown keys";
-@@ -608,7 +622,7 @@ enum sec_status
- dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* dnskey,
- uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate)
-+ sldns_pkt_section section, struct module_qstate* qstate, int* verified)
- {
- enum sec_status sec;
- size_t i, num;
-@@ -616,6 +630,7 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
- /* make sure that for all DNSKEY algorithms there are valid sigs */
- struct algo_needs needs;
- int alg;
-+ *verified = 0;
-
- num = rrset_get_sigcount(rrset);
- if(num == 0) {
-@@ -640,7 +655,7 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
- for(i=0; i<num; i++) {
- sec = dnskeyset_verify_rrset_sig(env, ve, *env->now, rrset,
- dnskey, i, &sortree, reason, reason_bogus,
-- section, qstate);
-+ section, qstate, verified);
- /* see which algorithm has been fixed up */
- if(sec == sec_status_secure) {
- if(!sigalg)
-@@ -652,6 +667,13 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
- algo_needs_set_bogus(&needs,
- (uint8_t)rrset_get_sig_algo(rrset, i));
- }
-+ if(*verified > MAX_VALIDATE_RRSIGS) {
-+ verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
-+ *reason = "too many RRSIG validations";
-+ if(reason_bogus)
-+ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
-+ return sec_status_bogus;
-+ }
- }
- if(sigalg && (alg=algo_needs_missing(&needs)) != 0) {
- verbose(VERB_ALGO, "rrset failed to verify: "
-@@ -690,6 +712,7 @@ dnskey_verify_rrset(struct module_env* env, struct val_env* ve,
- int buf_canon = 0;
- uint16_t tag = dnskey_calc_keytag(dnskey, dnskey_idx);
- int algo = dnskey_get_algo(dnskey, dnskey_idx);
-+ int numverified = 0;
-
- num = rrset_get_sigcount(rrset);
- if(num == 0) {
-@@ -713,8 +736,16 @@ dnskey_verify_rrset(struct module_env* env, struct val_env* ve,
- if(sec == sec_status_secure)
- return sec;
- numchecked ++;
-+ numverified ++;
- if(sec == sec_status_indeterminate)
- numindeterminate ++;
-+ if(numverified > MAX_VALIDATE_RRSIGS) {
-+ verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
-+ *reason = "too many RRSIG validations";
-+ if(reason_bogus)
-+ *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
-+ return sec_status_bogus;
-+ }
- }
- verbose(VERB_ALGO, "rrset failed to verify: all signatures are bogus");
- if(!numchecked) {
-diff --git a/validator/val_sigcrypt.h b/validator/val_sigcrypt.h
-index 7f52b71e..1a3d8fcb 100644
---- a/validator/val_sigcrypt.h
-+++ b/validator/val_sigcrypt.h
-@@ -260,6 +260,7 @@ uint16_t dnskey_get_flags(struct ub_packed_rrset_key* k, size_t idx);
- * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
- * @param section: section of packet where this rrset comes from.
- * @param qstate: qstate with region.
-+ * @param verified: if not NULL the number of RRSIG validations is returned.
- * @return SECURE if one key in the set verifies one rrsig.
- * UNCHECKED on allocation errors, unsupported algorithms, malformed data,
- * and BOGUS on verification failures (no keys match any signatures).
-@@ -268,7 +269,7 @@ enum sec_status dnskeyset_verify_rrset(struct module_env* env,
- struct val_env* ve, struct ub_packed_rrset_key* rrset,
- struct ub_packed_rrset_key* dnskey, uint8_t* sigalg,
- char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate);
-+ sldns_pkt_section section, struct module_qstate* qstate, int* verified);
-
-
- /**
-diff --git a/validator/val_utils.c b/validator/val_utils.c
-index e2319ee2..cb37ea00 100644
---- a/validator/val_utils.c
-+++ b/validator/val_utils.c
-@@ -58,6 +58,10 @@
- #include "sldns/wire2str.h"
- #include "sldns/parseutil.h"
-
-+/** Maximum allowed digest match failures per DS, for DNSKEYs with the same
-+ * properties */
-+#define MAX_DS_MATCH_FAILURES 4
-+
- enum val_classification
- val_classify_response(uint16_t query_flags, struct query_info* origqinf,
- struct query_info* qinf, struct reply_info* rep, size_t skip)
-@@ -336,7 +340,8 @@ static enum sec_status
- val_verify_rrset(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* keys,
- uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate)
-+ sldns_pkt_section section, struct module_qstate* qstate,
-+ int *verified)
- {
- enum sec_status sec;
- struct packed_rrset_data* d = (struct packed_rrset_data*)rrset->
-@@ -346,6 +351,7 @@ val_verify_rrset(struct module_env* env, struct val_env* ve,
- log_nametypeclass(VERB_ALGO, "verify rrset cached",
- rrset->rk.dname, ntohs(rrset->rk.type),
- ntohs(rrset->rk.rrset_class));
-+ *verified = 0;
- return d->security;
- }
- /* check in the cache if verification has already been done */
-@@ -354,12 +360,13 @@ val_verify_rrset(struct module_env* env, struct val_env* ve,
- log_nametypeclass(VERB_ALGO, "verify rrset from cache",
- rrset->rk.dname, ntohs(rrset->rk.type),
- ntohs(rrset->rk.rrset_class));
-+ *verified = 0;
- return d->security;
- }
- log_nametypeclass(VERB_ALGO, "verify rrset", rrset->rk.dname,
- ntohs(rrset->rk.type), ntohs(rrset->rk.rrset_class));
- sec = dnskeyset_verify_rrset(env, ve, rrset, keys, sigalg, reason,
-- reason_bogus, section, qstate);
-+ reason_bogus, section, qstate, verified);
- verbose(VERB_ALGO, "verify result: %s", sec_status_to_string(sec));
- regional_free_all(env->scratch);
-
-@@ -393,7 +400,8 @@ enum sec_status
- val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
- struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey,
- char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate)
-+ sldns_pkt_section section, struct module_qstate* qstate,
-+ int* verified)
- {
- /* temporary dnskey rrset-key */
- struct ub_packed_rrset_key dnskey;
-@@ -407,7 +415,7 @@ val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
- dnskey.entry.key = &dnskey;
- dnskey.entry.data = kd->rrset_data;
- sec = val_verify_rrset(env, ve, rrset, &dnskey, kd->algo, reason,
-- reason_bogus, section, qstate);
-+ reason_bogus, section, qstate, verified);
- return sec;
- }
-
-@@ -439,6 +447,12 @@ verify_dnskeys_with_ds_rr(struct module_env* env, struct val_env* ve,
- if(!ds_digest_match_dnskey(env, dnskey_rrset, i, ds_rrset,
- ds_idx)) {
- verbose(VERB_ALGO, "DS match attempt failed");
-+ if(numchecked > numhashok + MAX_DS_MATCH_FAILURES) {
-+ verbose(VERB_ALGO, "DS match attempt reached "
-+ "MAX_DS_MATCH_FAILURES (%d); bogus",
-+ MAX_DS_MATCH_FAILURES);
-+ return sec_status_bogus;
-+ }
- continue;
- }
- numhashok++;
-diff --git a/validator/val_utils.h b/validator/val_utils.h
-index 83e3d0ad..e8cdcefa 100644
---- a/validator/val_utils.h
-+++ b/validator/val_utils.h
-@@ -124,12 +124,14 @@ void val_find_signer(enum val_classification subtype,
- * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
- * @param section: section of packet where this rrset comes from.
- * @param qstate: qstate with region.
-+ * @param verified: if not NULL, the number of RRSIG validations is returned.
- * @return security status of verification.
- */
- enum sec_status val_verify_rrset_entry(struct module_env* env,
- struct val_env* ve, struct ub_packed_rrset_key* rrset,
- struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus,
-- sldns_pkt_section section, struct module_qstate* qstate);
-+ sldns_pkt_section section, struct module_qstate* qstate,
-+ int* verified);
-
- /**
- * Verify DNSKEYs with DS rrset. Like val_verify_new_DNSKEYs but
-diff --git a/validator/validator.c b/validator/validator.c
-index 1723afef..f1f7be34 100644
---- a/validator/validator.c
-+++ b/validator/validator.c
-@@ -64,10 +64,15 @@
- #include "sldns/wire2str.h"
- #include "sldns/str2wire.h"
-
-+/** Max number of RRSIGs to validate at once, suspend query for later. */
-+#define MAX_VALIDATE_AT_ONCE 8
-+/** Max number of validation suspends allowed, error out otherwise. */
-+#define MAX_VALIDATION_SUSPENDS 16
-+
- /* forward decl for cache response and normal super inform calls of a DS */
- static void process_ds_response(struct module_qstate* qstate,
- struct val_qstate* vq, int id, int rcode, struct dns_msg* msg,
-- struct query_info* qinfo, struct sock_list* origin);
-+ struct query_info* qinfo, struct sock_list* origin, int* suspend);
-
-
- /* Updates the suplied EDE (RFC8914) code selectively so we don't loose
-@@ -281,6 +286,21 @@ val_new(struct module_qstate* qstate, int id)
- return val_new_getmsg(qstate, vq);
- }
-
-+/** reset validator query state for query restart */
-+static void
-+val_restart(struct val_qstate* vq)
-+{
-+ struct comm_timer* temp_timer;
-+ int restart_count;
-+ if(!vq) return;
-+ temp_timer = vq->suspend_timer;
-+ restart_count = vq->restart_count+1;
-+ memset(vq, 0, sizeof(*vq));
-+ vq->suspend_timer = temp_timer;
-+ vq->restart_count = restart_count;
-+ vq->state = VAL_INIT_STATE;
-+}
-+
- /**
- * Exit validation with an error status
- *
-@@ -587,30 +607,42 @@ prime_trust_anchor(struct module_qstate* qstate, struct val_qstate* vq,
- * completed.
- *
- * @param qstate: query state.
-+ * @param vq: validator query state.
- * @param env: module env for verify.
- * @param ve: validator env for verify.
- * @param qchase: query that was made.
- * @param chase_reply: answer to validate.
- * @param key_entry: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- * @return false if any of the rrsets in the an or ns sections of the message
- * fail to verify. The message is then set to bogus.
- */
- static int
--validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
-- struct val_env* ve, struct query_info* qchase,
-- struct reply_info* chase_reply, struct key_entry_key* key_entry)
-+validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq,
-+ struct module_env* env, struct val_env* ve, struct query_info* qchase,
-+ struct reply_info* chase_reply, struct key_entry_key* key_entry,
-+ int* suspend)
- {
- uint8_t* sname;
- size_t i, slen;
- struct ub_packed_rrset_key* s;
- enum sec_status sec;
-- int dname_seen = 0;
-+ int dname_seen = 0, num_verifies = 0, verified, have_state = 0;
- char* reason = NULL;
- sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
-+ *suspend = 0;
-+ if(vq->msg_signatures_state) {
-+ /* Pick up the state, and reset it, may not be needed now. */
-+ vq->msg_signatures_state = 0;
-+ have_state = 1;
-+ }
-
- /* validate the ANSWER section */
- for(i=0; i<chase_reply->an_numrrsets; i++) {
-+ if(have_state && i <= vq->msg_signatures_index)
-+ continue;
- s = chase_reply->rrsets[i];
- /* Skip the CNAME following a (validated) DNAME.
- * Because of the normalization routines in the iterator,
-@@ -629,7 +661,7 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
-
- /* Verify the answer rrset */
- sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-- &reason_bogus, LDNS_SECTION_ANSWER, qstate);
-+ &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
- /* If the (answer) rrset failed to validate, then this
- * message is BAD. */
- if(sec != sec_status_secure) {
-@@ -654,14 +686,33 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
- ntohs(s->rk.type) == LDNS_RR_TYPE_DNAME) {
- dname_seen = 1;
- }
-+ num_verifies += verified;
-+ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
-+ i+1 < (env->cfg->val_clean_additional?
-+ chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
-+ chase_reply->rrset_count)) {
-+ /* If the number of RRSIGs exceeds the maximum in
-+ * one go, suspend. Only suspend if there is a next
-+ * rrset to verify, i+1<loopmax. Store where to
-+ * continue later. */
-+ *suspend = 1;
-+ vq->msg_signatures_state = 1;
-+ vq->msg_signatures_index = i;
-+ verbose(VERB_ALGO, "msg signature validation "
-+ "suspended");
-+ return 0;
-+ }
- }
-
- /* validate the AUTHORITY section */
- for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
- chase_reply->ns_numrrsets; i++) {
-+ if(have_state && i <= vq->msg_signatures_index)
-+ continue;
- s = chase_reply->rrsets[i];
- sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-- &reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
-+ &reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
-+ &verified);
- /* If anything in the authority section fails to be secure,
- * we have a bad message. */
- if(sec != sec_status_secure) {
-@@ -675,6 +726,18 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
- update_reason_bogus(chase_reply, reason_bogus);
- return 0;
- }
-+ num_verifies += verified;
-+ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
-+ i+1 < (env->cfg->val_clean_additional?
-+ chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
-+ chase_reply->rrset_count)) {
-+ *suspend = 1;
-+ vq->msg_signatures_state = 1;
-+ vq->msg_signatures_index = i;
-+ verbose(VERB_ALGO, "msg signature validation "
-+ "suspended");
-+ return 0;
-+ }
- }
-
- /* If set, the validator should clean the additional section of
-@@ -684,22 +747,103 @@ validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
- /* attempt to validate the ADDITIONAL section rrsets */
- for(i=chase_reply->an_numrrsets+chase_reply->ns_numrrsets;
- i<chase_reply->rrset_count; i++) {
-+ if(have_state && i <= vq->msg_signatures_index)
-+ continue;
- s = chase_reply->rrsets[i];
- /* only validate rrs that have signatures with the key */
- /* leave others unchecked, those get removed later on too */
- val_find_rrset_signer(s, &sname, &slen);
-
-+ verified = 0;
- if(sname && query_dname_compare(sname, key_entry->name)==0)
- (void)val_verify_rrset_entry(env, ve, s, key_entry,
-- &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate);
-+ &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate,
-+ &verified);
- /* the additional section can fail to be secure,
- * it is optional, check signature in case we need
- * to clean the additional section later. */
-+ num_verifies += verified;
-+ if(num_verifies > MAX_VALIDATE_AT_ONCE &&
-+ i+1 < chase_reply->rrset_count) {
-+ *suspend = 1;
-+ vq->msg_signatures_state = 1;
-+ vq->msg_signatures_index = i;
-+ verbose(VERB_ALGO, "msg signature validation "
-+ "suspended");
-+ return 0;
-+ }
- }
-
- return 1;
- }
-
-+void
-+validate_suspend_timer_cb(void* arg)
-+{
-+ struct module_qstate* qstate = (struct module_qstate*)arg;
-+ verbose(VERB_ALGO, "validate_suspend timer, continue");
-+ mesh_run(qstate->env->mesh, qstate->mesh_info, module_event_pass,
-+ NULL);
-+}
-+
-+/** Setup timer to continue validation of msg signatures later */
-+static int
-+validate_suspend_setup_timer(struct module_qstate* qstate,
-+ struct val_qstate* vq, int id, enum val_state resume_state)
-+{
-+ struct timeval tv;
-+ int usec, slack, base;
-+ if(vq->suspend_count >= MAX_VALIDATION_SUSPENDS) {
-+ verbose(VERB_ALGO, "validate_suspend timer: "
-+ "reached MAX_VALIDATION_SUSPENDS (%d); error out",
-+ MAX_VALIDATION_SUSPENDS);
-+ errinf(qstate, "max validation suspends reached, "
-+ "too many RRSIG validations");
-+ return 0;
-+ }
-+ verbose(VERB_ALGO, "validate_suspend timer, set for suspend");
-+ vq->state = resume_state;
-+ qstate->ext_state[id] = module_wait_reply;
-+ if(!vq->suspend_timer) {
-+ vq->suspend_timer = comm_timer_create(
-+ qstate->env->worker_base,
-+ validate_suspend_timer_cb, qstate);
-+ if(!vq->suspend_timer) {
-+ log_err("validate_suspend_setup_timer: "
-+ "out of memory for comm_timer_create");
-+ return 0;
-+ }
-+ }
-+ /* The timer is activated later, after other events in the event
-+ * loop have been processed. The query state can also be deleted,
-+ * when the list is full and query states are dropped. */
-+ /* Extend wait time if there are a lot of queries or if this one
-+ * is taking long, to keep around cpu time for ordinary queries. */
-+ usec = 50000; /* 50 msec */
-+ slack = 0;
-+ if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states)
-+ slack += 3;
-+ else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/2)
-+ slack += 2;
-+ else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/4)
-+ slack += 1;
-+ if(vq->suspend_count > 3)
-+ slack += 3;
-+ else if(vq->suspend_count > 0)
-+ slack += vq->suspend_count;
-+ if(slack != 0 && slack <= 12 /* No numeric overflow. */) {
-+ usec = usec << slack;
-+ }
-+ /* Spread such timeouts within 90%-100% of the original timer. */
-+ base = usec * 9/10;
-+ usec = base + ub_random_max(qstate->env->rnd, usec-base);
-+ tv.tv_usec = (usec % 1000000);
-+ tv.tv_sec = (usec / 1000000);
-+ vq->suspend_count ++;
-+ comm_timer_set(vq->suspend_timer, &tv);
-+ return 1;
-+}
-+
- /**
- * Detect wrong truncated response (say from BIND 9.6.1 that is forwarding
- * and saw the NS record without signatures from a referral).
-@@ -798,11 +942,17 @@ remove_spurious_authority(struct reply_info* chase_reply,
- * @param chase_reply: answer to that query to validate.
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_positive_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey)
-+ struct key_entry_key* kkey, struct module_qstate* qstate,
-+ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
- {
- uint8_t* wc = NULL;
- size_t wl;
-@@ -811,6 +961,7 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
- int nsec3s_seen = 0;
- size_t i;
- struct ub_packed_rrset_key* s;
-+ *suspend = 0;
-
- /* validate the ANSWER section - this will be the answer itself */
- for(i=0; i<chase_reply->an_numrrsets; i++) {
-@@ -862,17 +1013,23 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
- /* If this was a positive wildcard response that we haven't already
- * proven, and we have NSEC3 records, try to prove it using the NSEC3
- * records. */
-- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-- enum sec_status sec = nsec3_prove_wildcard(env, ve,
-+ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
-+ enum sec_status sec = nsec3_prove_wildcard(env, ve,
- chase_reply->rrsets+chase_reply->an_numrrsets,
-- chase_reply->ns_numrrsets, qchase, kkey, wc);
-+ chase_reply->ns_numrrsets, qchase, kkey, wc,
-+ &vq->nsec3_cache_table, nsec3_calculations);
- if(sec == sec_status_insecure) {
- verbose(VERB_ALGO, "Positive wildcard response is "
- "insecure");
- chase_reply->security = sec_status_insecure;
- return;
-- } else if(sec == sec_status_secure)
-+ } else if(sec == sec_status_secure) {
- wc_NSEC_ok = 1;
-+ } else if(sec == sec_status_unchecked) {
-+ *suspend = 1;
-+ return;
-+ }
- }
-
- /* If after all this, we still haven't proven the positive wildcard
-@@ -904,11 +1061,17 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
- * @param chase_reply: answer to that query to validate.
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_nodata_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey)
-+ struct key_entry_key* kkey, struct module_qstate* qstate,
-+ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
- {
- /* Since we are here, there must be nothing in the ANSWER section to
- * validate. */
-@@ -925,6 +1088,7 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
- int nsec3s_seen = 0; /* nsec3s seen */
- struct ub_packed_rrset_key* s;
- size_t i;
-+ *suspend = 0;
-
- for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
- chase_reply->ns_numrrsets; i++) {
-@@ -963,16 +1127,23 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
- }
- }
-
-- if(!has_valid_nsec && nsec3s_seen) {
-+ if(!has_valid_nsec && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
- enum sec_status sec = nsec3_prove_nodata(env, ve,
- chase_reply->rrsets+chase_reply->an_numrrsets,
-- chase_reply->ns_numrrsets, qchase, kkey);
-+ chase_reply->ns_numrrsets, qchase, kkey,
-+ &vq->nsec3_cache_table, nsec3_calculations);
- if(sec == sec_status_insecure) {
- verbose(VERB_ALGO, "NODATA response is insecure");
- chase_reply->security = sec_status_insecure;
- return;
-- } else if(sec == sec_status_secure)
-+ } else if(sec == sec_status_secure) {
- has_valid_nsec = 1;
-+ } else if(sec == sec_status_unchecked) {
-+ /* check is incomplete; suspend */
-+ *suspend = 1;
-+ return;
-+ }
- }
-
- if(!has_valid_nsec) {
-@@ -1004,11 +1175,18 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
- * @param rcode: adjusted RCODE, in case of RCODE/proof mismatch leniency.
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_nameerror_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey, int* rcode)
-+ struct key_entry_key* kkey, int* rcode,
-+ struct module_qstate* qstate, struct val_qstate* vq,
-+ int* nsec3_calculations, int* suspend)
- {
- int has_valid_nsec = 0;
- int has_valid_wnsec = 0;
-@@ -1018,6 +1196,7 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
- uint8_t* ce;
- int ce_labs = 0;
- int prev_ce_labs = 0;
-+ *suspend = 0;
-
- for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
- chase_reply->ns_numrrsets; i++) {
-@@ -1047,13 +1226,18 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
- nsec3s_seen = 1;
- }
-
-- if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen) {
-+ if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
- /* use NSEC3 proof, both answer and auth rrsets, in case
- * NSEC3s end up in the answer (due to qtype=NSEC3 or so) */
- chase_reply->security = nsec3_prove_nameerror(env, ve,
- chase_reply->rrsets, chase_reply->an_numrrsets+
-- chase_reply->ns_numrrsets, qchase, kkey);
-- if(chase_reply->security != sec_status_secure) {
-+ chase_reply->ns_numrrsets, qchase, kkey,
-+ &vq->nsec3_cache_table, nsec3_calculations);
-+ if(chase_reply->security == sec_status_unchecked) {
-+ *suspend = 1;
-+ return;
-+ } else if(chase_reply->security != sec_status_secure) {
- verbose(VERB_QUERY, "NameError response failed nsec, "
- "nsec3 proof was %s", sec_status_to_string(
- chase_reply->security));
-@@ -1065,26 +1249,34 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
-
- /* If the message fails to prove either condition, it is bogus. */
- if(!has_valid_nsec) {
-+ validate_nodata_response(env, ve, qchase, chase_reply, kkey,
-+ qstate, vq, nsec3_calculations, suspend);
-+ if(*suspend) return;
- verbose(VERB_QUERY, "NameError response has failed to prove: "
- "qname does not exist");
-- chase_reply->security = sec_status_bogus;
-- update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
- /* Be lenient with RCODE in NSEC NameError responses */
-- validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-- if (chase_reply->security == sec_status_secure)
-+ if(chase_reply->security == sec_status_secure) {
- *rcode = LDNS_RCODE_NOERROR;
-+ } else {
-+ chase_reply->security = sec_status_bogus;
-+ update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
-+ }
- return;
- }
-
- if(!has_valid_wnsec) {
-+ validate_nodata_response(env, ve, qchase, chase_reply, kkey,
-+ qstate, vq, nsec3_calculations, suspend);
-+ if(*suspend) return;
- verbose(VERB_QUERY, "NameError response has failed to prove: "
- "covering wildcard does not exist");
-- chase_reply->security = sec_status_bogus;
-- update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
- /* Be lenient with RCODE in NSEC NameError responses */
-- validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-- if (chase_reply->security == sec_status_secure)
-+ if (chase_reply->security == sec_status_secure) {
- *rcode = LDNS_RCODE_NOERROR;
-+ } else {
-+ chase_reply->security = sec_status_bogus;
-+ update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
-+ }
- return;
- }
-
-@@ -1144,11 +1336,17 @@ validate_referral_response(struct reply_info* chase_reply)
- * @param chase_reply: answer to that query to validate.
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_any_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey)
-+ struct key_entry_key* kkey, struct module_qstate* qstate,
-+ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
- {
- /* all answer and auth rrsets already verified */
- /* but check if a wildcard response is given, then check NSEC/NSEC3
-@@ -1159,6 +1357,7 @@ validate_any_response(struct module_env* env, struct val_env* ve,
- int nsec3s_seen = 0;
- size_t i;
- struct ub_packed_rrset_key* s;
-+ *suspend = 0;
-
- if(qchase->qtype != LDNS_RR_TYPE_ANY) {
- log_err("internal error: ANY validation called for non-ANY");
-@@ -1213,19 +1412,25 @@ validate_any_response(struct module_env* env, struct val_env* ve,
- /* If this was a positive wildcard response that we haven't already
- * proven, and we have NSEC3 records, try to prove it using the NSEC3
- * records. */
-- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-+ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
- /* look both in answer and auth section for NSEC3s */
-- enum sec_status sec = nsec3_prove_wildcard(env, ve,
-+ enum sec_status sec = nsec3_prove_wildcard(env, ve,
- chase_reply->rrsets,
-- chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
-- qchase, kkey, wc);
-+ chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
-+ qchase, kkey, wc, &vq->nsec3_cache_table,
-+ nsec3_calculations);
- if(sec == sec_status_insecure) {
- verbose(VERB_ALGO, "Positive ANY wildcard response is "
- "insecure");
- chase_reply->security = sec_status_insecure;
- return;
-- } else if(sec == sec_status_secure)
-+ } else if(sec == sec_status_secure) {
- wc_NSEC_ok = 1;
-+ } else if(sec == sec_status_unchecked) {
-+ *suspend = 1;
-+ return;
-+ }
- }
-
- /* If after all this, we still haven't proven the positive wildcard
-@@ -1258,11 +1463,17 @@ validate_any_response(struct module_env* env, struct val_env* ve,
- * @param chase_reply: answer to that query to validate.
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_cname_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey)
-+ struct key_entry_key* kkey, struct module_qstate* qstate,
-+ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
- {
- uint8_t* wc = NULL;
- size_t wl;
-@@ -1270,6 +1481,7 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
- int nsec3s_seen = 0;
- size_t i;
- struct ub_packed_rrset_key* s;
-+ *suspend = 0;
-
- /* validate the ANSWER section - this will be the CNAME (+DNAME) */
- for(i=0; i<chase_reply->an_numrrsets; i++) {
-@@ -1334,17 +1546,23 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
- /* If this was a positive wildcard response that we haven't already
- * proven, and we have NSEC3 records, try to prove it using the NSEC3
- * records. */
-- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-- enum sec_status sec = nsec3_prove_wildcard(env, ve,
-+ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
-+ enum sec_status sec = nsec3_prove_wildcard(env, ve,
- chase_reply->rrsets+chase_reply->an_numrrsets,
-- chase_reply->ns_numrrsets, qchase, kkey, wc);
-+ chase_reply->ns_numrrsets, qchase, kkey, wc,
-+ &vq->nsec3_cache_table, nsec3_calculations);
- if(sec == sec_status_insecure) {
- verbose(VERB_ALGO, "wildcard CNAME response is "
- "insecure");
- chase_reply->security = sec_status_insecure;
- return;
-- } else if(sec == sec_status_secure)
-+ } else if(sec == sec_status_secure) {
- wc_NSEC_ok = 1;
-+ } else if(sec == sec_status_unchecked) {
-+ *suspend = 1;
-+ return;
-+ }
- }
-
- /* If after all this, we still haven't proven the positive wildcard
-@@ -1375,11 +1593,17 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
- * @param chase_reply: answer to that query to validate.
- * @param kkey: the key entry, which is trusted, and which matches
- * the signer of the answer. The key entry isgood().
-+ * @param qstate: query state for the region.
-+ * @param vq: validator state for the nsec3 cache table.
-+ * @param nsec3_calculations: current nsec3 hash calculations.
-+ * @param suspend: returned true if the task takes too long and needs to
-+ * suspend to continue the effort later.
- */
- static void
- validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
- struct query_info* qchase, struct reply_info* chase_reply,
-- struct key_entry_key* kkey)
-+ struct key_entry_key* kkey, struct module_qstate* qstate,
-+ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
- {
- int nodata_valid_nsec = 0; /* If true, then NODATA has been proven.*/
- uint8_t* ce = NULL; /* for wildcard nodata responses. This is the
-@@ -1393,6 +1617,7 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
- uint8_t* nsec_ce; /* Used to find the NSEC with the longest ce */
- int ce_labs = 0;
- int prev_ce_labs = 0;
-+ *suspend = 0;
-
- /* the AUTHORITY section */
- for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
-@@ -1458,11 +1683,13 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
- update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
- return;
- }
-- if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen) {
-+ if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen &&
-+ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
- int nodata;
- enum sec_status sec = nsec3_prove_nxornodata(env, ve,
- chase_reply->rrsets+chase_reply->an_numrrsets,
-- chase_reply->ns_numrrsets, qchase, kkey, &nodata);
-+ chase_reply->ns_numrrsets, qchase, kkey, &nodata,
-+ &vq->nsec3_cache_table, nsec3_calculations);
- if(sec == sec_status_insecure) {
- verbose(VERB_ALGO, "CNAMEchain to noanswer response "
- "is insecure");
-@@ -1472,6 +1699,9 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
- if(nodata)
- nodata_valid_nsec = 1;
- else nxdomain_valid_nsec = 1;
-+ } else if(sec == sec_status_unchecked) {
-+ *suspend = 1;
-+ return;
- }
- }
-
-@@ -1822,13 +2052,37 @@ processFindKey(struct module_qstate* qstate, struct val_qstate* vq, int id)
- * Uses negative cache for NSEC3 lookup of DS responses. */
- /* only if cache not blacklisted, of course */
- struct dns_msg* msg;
-- if(!qstate->blacklist && !vq->chain_blacklist &&
-+ int suspend;
-+ if(vq->sub_ds_msg) {
-+ /* We have a suspended DS reply from a sub-query;
-+ * process it. */
-+ verbose(VERB_ALGO, "Process suspended sub DS response");
-+ msg = vq->sub_ds_msg;
-+ process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
-+ msg, &msg->qinfo, NULL, &suspend);
-+ if(suspend) {
-+ /* we'll come back here later to continue */
-+ if(!validate_suspend_setup_timer(qstate, vq,
-+ id, VAL_FINDKEY_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
-+ vq->sub_ds_msg = NULL;
-+ return 1; /* continue processing ds-response results */
-+ } else if(!qstate->blacklist && !vq->chain_blacklist &&
- (msg=val_find_DS(qstate->env, target_key_name,
- target_key_len, vq->qchase.qclass, qstate->region,
- vq->key_entry->name)) ) {
- verbose(VERB_ALGO, "Process cached DS response");
- process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
-- msg, &msg->qinfo, NULL);
-+ msg, &msg->qinfo, NULL, &suspend);
-+ if(suspend) {
-+ /* we'll come back here later to continue */
-+ if(!validate_suspend_setup_timer(qstate, vq,
-+ id, VAL_FINDKEY_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- return 1; /* continue processing ds-response results */
- }
- if(!generate_request(qstate, id, target_key_name,
-@@ -1871,7 +2125,7 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- struct val_env* ve, int id)
- {
- enum val_classification subtype;
-- int rcode;
-+ int rcode, suspend, nsec3_calculations = 0;
-
- if(!vq->key_entry) {
- verbose(VERB_ALGO, "validate: no key entry, failed");
-@@ -1926,8 +2180,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
-
- /* check signatures in the message;
- * answer and authority must be valid, additional is only checked. */
-- if(!validate_msg_signatures(qstate, qstate->env, ve, &vq->qchase,
-- vq->chase_reply, vq->key_entry)) {
-+ if(!validate_msg_signatures(qstate, vq, qstate->env, ve, &vq->qchase,
-+ vq->chase_reply, vq->key_entry, &suspend)) {
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate, vq,
-+ id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- /* workaround bad recursor out there that truncates (even
- * with EDNS4k) to 512 by removing RRSIG from auth section
- * for positive replies*/
-@@ -1956,7 +2216,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- case VAL_CLASS_POSITIVE:
- verbose(VERB_ALGO, "Validating a positive response");
- validate_positive_response(qstate->env, ve,
-- &vq->qchase, vq->chase_reply, vq->key_entry);
-+ &vq->qchase, vq->chase_reply, vq->key_entry,
-+ qstate, vq, &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(positive): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -1965,7 +2232,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- case VAL_CLASS_NODATA:
- verbose(VERB_ALGO, "Validating a nodata response");
- validate_nodata_response(qstate->env, ve,
-- &vq->qchase, vq->chase_reply, vq->key_entry);
-+ &vq->qchase, vq->chase_reply, vq->key_entry,
-+ qstate, vq, &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(nodata): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -1975,7 +2249,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- rcode = (int)FLAGS_GET_RCODE(vq->orig_msg->rep->flags);
- verbose(VERB_ALGO, "Validating a nxdomain response");
- validate_nameerror_response(qstate->env, ve,
-- &vq->qchase, vq->chase_reply, vq->key_entry, &rcode);
-+ &vq->qchase, vq->chase_reply, vq->key_entry, &rcode,
-+ qstate, vq, &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(nxdomain): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -1986,7 +2267,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- case VAL_CLASS_CNAME:
- verbose(VERB_ALGO, "Validating a cname response");
- validate_cname_response(qstate->env, ve,
-- &vq->qchase, vq->chase_reply, vq->key_entry);
-+ &vq->qchase, vq->chase_reply, vq->key_entry,
-+ qstate, vq, &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(cname): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -1996,7 +2284,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- verbose(VERB_ALGO, "Validating a cname noanswer "
- "response");
- validate_cname_noanswer_response(qstate->env, ve,
-- &vq->qchase, vq->chase_reply, vq->key_entry);
-+ &vq->qchase, vq->chase_reply, vq->key_entry,
-+ qstate, vq, &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(cname_noanswer): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -2013,8 +2308,15 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
- case VAL_CLASS_ANY:
- verbose(VERB_ALGO, "Validating a positive ANY "
- "response");
-- validate_any_response(qstate->env, ve, &vq->qchase,
-- vq->chase_reply, vq->key_entry);
-+ validate_any_response(qstate->env, ve, &vq->qchase,
-+ vq->chase_reply, vq->key_entry, qstate, vq,
-+ &nsec3_calculations, &suspend);
-+ if(suspend) {
-+ if(!validate_suspend_setup_timer(qstate,
-+ vq, id, VAL_VALIDATE_STATE))
-+ return val_error(qstate, id);
-+ return 0;
-+ }
- verbose(VERB_DETAIL, "validate(positive_any): %s",
- sec_status_to_string(
- vq->chase_reply->security));
-@@ -2123,16 +2425,13 @@ processFinished(struct module_qstate* qstate, struct val_qstate* vq,
- if(vq->orig_msg->rep->security == sec_status_bogus) {
- /* see if we can try again to fetch data */
- if(vq->restart_count < ve->max_restart) {
-- int restart_count = vq->restart_count+1;
- verbose(VERB_ALGO, "validation failed, "
- "blacklist and retry to fetch data");
- val_blacklist(&qstate->blacklist, qstate->region,
- qstate->reply_origin, 0);
- qstate->reply_origin = NULL;
- qstate->errinf = NULL;
-- memset(vq, 0, sizeof(*vq));
-- vq->restart_count = restart_count;
-- vq->state = VAL_INIT_STATE;
-+ val_restart(vq);
- verbose(VERB_ALGO, "pass back to next module");
- qstate->ext_state[id] = module_restart_next;
- return 0;
-@@ -2440,7 +2739,10 @@ primeResponseToKE(struct ub_packed_rrset_key* dnskey_rrset,
- * DS response indicated an end to secure space, is_good if the DS
- * validated. It returns ke=NULL if the DS response indicated that the
- * request wasn't a delegation point.
-- * @return 0 on servfail error (malloc failure).
-+ * @return
-+ * 0 on success,
-+ * 1 on servfail error (malloc failure),
-+ * 2 on NSEC3 suspend.
- */
- static int
- ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
-@@ -2451,6 +2753,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- char* reason = NULL;
- sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
- enum val_classification subtype;
-+ int verified;
- if(rcode != LDNS_RCODE_NOERROR) {
- char rc[16];
- rc[0]=0;
-@@ -2479,7 +2782,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- /* Verify only returns BOGUS or SECURE. If the rrset is
- * bogus, then we are done. */
- sec = val_verify_rrset_entry(qstate->env, ve, ds,
-- vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate);
-+ vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
- if(sec != sec_status_secure) {
- verbose(VERB_DETAIL, "DS rrset in DS response did "
- "not verify");
-@@ -2499,7 +2802,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- *ke = key_entry_create_null(qstate->region,
- qinfo->qname, qinfo->qname_len, qinfo->qclass,
- ub_packed_rrset_ttl(ds), *qstate->env->now);
-- return (*ke) != NULL;
-+ return (*ke) == NULL;
- }
-
- /* Otherwise, we return the positive response. */
-@@ -2507,7 +2810,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- *ke = key_entry_create_rrset(qstate->region,
- qinfo->qname, qinfo->qname_len, qinfo->qclass, ds,
- NULL, *qstate->env->now);
-- return (*ke) != NULL;
-+ return (*ke) == NULL;
- } else if(subtype == VAL_CLASS_NODATA ||
- subtype == VAL_CLASS_NAMEERROR) {
- /* NODATA means that the qname exists, but that there was
-@@ -2539,12 +2842,12 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- qinfo->qname, qinfo->qname_len,
- qinfo->qclass, proof_ttl,
- *qstate->env->now);
-- return (*ke) != NULL;
-+ return (*ke) == NULL;
- case sec_status_insecure:
- verbose(VERB_DETAIL, "NSEC RRset for the "
- "referral proved not a delegation point");
- *ke = NULL;
-- return 1;
-+ return 0;
- case sec_status_bogus:
- verbose(VERB_DETAIL, "NSEC RRset for the "
- "referral did not prove no DS.");
-@@ -2556,10 +2859,17 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- break;
- }
-
-+ if(!nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
-+ log_err("malloc failure in ds_response_to_ke for "
-+ "NSEC3 cache");
-+ reason = "malloc failure";
-+ errinf_ede(qstate, reason, 0);
-+ goto return_bogus;
-+ }
- sec = nsec3_prove_nods(qstate->env, ve,
- msg->rep->rrsets + msg->rep->an_numrrsets,
- msg->rep->ns_numrrsets, qinfo, vq->key_entry, &reason,
-- &reason_bogus, qstate);
-+ &reason_bogus, qstate, &vq->nsec3_cache_table);
- switch(sec) {
- case sec_status_insecure:
- /* case insecure also continues to unsigned
-@@ -2572,18 +2882,19 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- qinfo->qname, qinfo->qname_len,
- qinfo->qclass, proof_ttl,
- *qstate->env->now);
-- return (*ke) != NULL;
-+ return (*ke) == NULL;
- case sec_status_indeterminate:
- verbose(VERB_DETAIL, "NSEC3s for the "
- "referral proved no delegation");
- *ke = NULL;
-- return 1;
-+ return 0;
- case sec_status_bogus:
- verbose(VERB_DETAIL, "NSEC3s for the "
- "referral did not prove no DS.");
- errinf_ede(qstate, reason, reason_bogus);
- goto return_bogus;
- case sec_status_unchecked:
-+ return 2;
- default:
- /* NSEC3 proof did not work */
- break;
-@@ -2620,13 +2931,14 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
- goto return_bogus;
- }
- sec = val_verify_rrset_entry(qstate->env, ve, cname,
-- vq->key_entry, &reason, NULL, LDNS_SECTION_ANSWER, qstate);
-+ vq->key_entry, &reason, NULL, LDNS_SECTION_ANSWER,
-+ qstate, &verified);
- if(sec == sec_status_secure) {
- verbose(VERB_ALGO, "CNAME validated, "
- "proof that DS does not exist");
- /* and that it is not a referral point */
- *ke = NULL;
-- return 1;
-+ return 0;
- }
- errinf(qstate, "CNAME in DS response was not secure.");
- errinf(qstate, reason);
-@@ -2649,7 +2961,7 @@ return_bogus:
- *ke = key_entry_create_bad(qstate->region, qinfo->qname,
- qinfo->qname_len, qinfo->qclass,
- BOGUS_KEY_TTL, *qstate->env->now);
-- return (*ke) != NULL;
-+ return (*ke) == NULL;
- }
-
- /**
-@@ -2670,17 +2982,31 @@ return_bogus:
- static void
- process_ds_response(struct module_qstate* qstate, struct val_qstate* vq,
- int id, int rcode, struct dns_msg* msg, struct query_info* qinfo,
-- struct sock_list* origin)
-+ struct sock_list* origin, int* suspend)
- {
- struct val_env* ve = (struct val_env*)qstate->env->modinfo[id];
- struct key_entry_key* dske = NULL;
- uint8_t* olds = vq->empty_DS_name;
-+ int ret;
-+ *suspend = 0;
- vq->empty_DS_name = NULL;
-- if(!ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske)) {
-+ ret = ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske);
-+ if(ret != 0) {
-+ switch(ret) {
-+ case 1:
- log_err("malloc failure in process_ds_response");
- vq->key_entry = NULL; /* make it error */
- vq->state = VAL_VALIDATE_STATE;
- return;
-+ case 2:
-+ *suspend = 1;
-+ return;
-+ default:
-+ log_err("unhandled error value for ds_response_to_ke");
-+ vq->key_entry = NULL; /* make it error */
-+ vq->state = VAL_VALIDATE_STATE;
-+ return;
-+ }
- }
- if(dske == NULL) {
- vq->empty_DS_name = regional_alloc_init(qstate->region,
-@@ -2927,9 +3253,26 @@ val_inform_super(struct module_qstate* qstate, int id,
- return;
- }
- if(qstate->qinfo.qtype == LDNS_RR_TYPE_DS) {
-+ int suspend;
- process_ds_response(super, vq, id, qstate->return_rcode,
-- qstate->return_msg, &qstate->qinfo,
-- qstate->reply_origin);
-+ qstate->return_msg, &qstate->qinfo,
-+ qstate->reply_origin, &suspend);
-+ /* If NSEC3 was needed during validation, NULL the NSEC3 cache;
-+ * it will be re-initiated if needed later on.
-+ * Validation (and the cache table) are happening/allocated in
-+ * the super qstate whilst the RRs are allocated (and pointed
-+ * to) in this sub qstate. */
-+ if(vq->nsec3_cache_table.ct) {
-+ vq->nsec3_cache_table.ct = NULL;
-+ }
-+ if(suspend) {
-+ /* deep copy the return_msg to vq->sub_ds_msg; it will
-+ * be resumed later in the super state with the caveat
-+ * that the initial calculations will be re-caclulated
-+ * and re-suspended there before continuing. */
-+ vq->sub_ds_msg = dns_msg_deepcopy_region(
-+ qstate->return_msg, super->region);
-+ }
- return;
- } else if(qstate->qinfo.qtype == LDNS_RR_TYPE_DNSKEY) {
- process_dnskey_response(super, vq, id, qstate->return_rcode,
-@@ -2943,8 +3286,15 @@ val_inform_super(struct module_qstate* qstate, int id,
- void
- val_clear(struct module_qstate* qstate, int id)
- {
-+ struct val_qstate* vq;
- if(!qstate)
- return;
-+ vq = (struct val_qstate*)qstate->minfo[id];
-+ if(vq) {
-+ if(vq->suspend_timer) {
-+ comm_timer_delete(vq->suspend_timer);
-+ }
-+ }
- /* everything is allocated in the region, so assign NULL */
- qstate->minfo[id] = NULL;
- }
-diff --git a/validator/validator.h b/validator/validator.h
-index 694e4c89..72f44b16 100644
---- a/validator/validator.h
-+++ b/validator/validator.h
-@@ -45,11 +45,13 @@
- #include "util/module.h"
- #include "util/data/msgreply.h"
- #include "validator/val_utils.h"
-+#include "validator/val_nsec3.h"
- struct val_anchors;
- struct key_cache;
- struct key_entry_key;
- struct val_neg_cache;
- struct config_strlist;
-+struct comm_timer;
-
- /**
- * This is the TTL to use when a trust anchor fails to prime. A trust anchor
-@@ -215,6 +217,19 @@ struct val_qstate {
-
- /** true if this state is waiting to prime a trust anchor */
- int wait_prime_ta;
-+
-+ /** State to continue with RRSIG validation in a message later */
-+ int msg_signatures_state;
-+ /** The rrset index for the msg signatures to continue from */
-+ size_t msg_signatures_index;
-+ /** Cache table for NSEC3 hashes */
-+ struct nsec3_cache_table nsec3_cache_table;
-+ /** DS message from sub if it got suspended from NSEC3 calculations */
-+ struct dns_msg* sub_ds_msg;
-+ /** The timer to resume processing msg signatures */
-+ struct comm_timer* suspend_timer;
-+ /** Number of suspends */
-+ int suspend_count;
- };
-
- /**
-@@ -262,4 +277,7 @@ void val_clear(struct module_qstate* qstate, int id);
- */
- size_t val_get_mem(struct module_env* env, int id);
-
-+/** Timer callback for msg signatures continue timer */
-+void validate_suspend_timer_cb(void* arg);
-+
- #endif /* VALIDATOR_VALIDATOR_H */
diff -Nru unbound-1.17.1/debian/patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch unbound-1.17.1/debian/patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch
--- unbound-1.17.1/debian/patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,1750 @@
+From: "W.C.A. Wijngaards" <wouter@nlnetlabs.nl>
+Date: Tue, 13 Feb 2024 13:02:43 +0100
+Subject: CVE-2023-50868, NSEC3 closest encloser proof can exhaust CPU.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/92f2a1ca690a44880f4c4fa70a4b5a4b029aaf1c
+Bug: https://nlnetlabs.nl/downloads/unbound/CVE-2023-50868.txt
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-50868
+---
+ services/cache/dns.c | 18 ++
+ services/cache/dns.h | 9 +
+ testcode/unitverify.c | 4 +-
+ testdata/val_nx_nsec3_collision.rpl | 3 +
+ util/fptr_wlist.c | 2 +-
+ validator/val_nsec3.c | 312 ++++++++++++++++++------
+ validator/val_nsec3.h | 60 ++++-
+ validator/validator.c | 353 +++++++++++++++++++++-------
+ validator/validator.h | 13 +-
+ 9 files changed, 607 insertions(+), 167 deletions(-)
+
+diff --git a/services/cache/dns.c b/services/cache/dns.c
+index 6fc9919ef..1ced11439 100644
+--- a/services/cache/dns.c
++++ b/services/cache/dns.c
+@@ -703,6 +703,24 @@ tomsg(struct module_env* env, struct query_info* q, struct reply_info* r,
+ return msg;
+ }
+
++struct dns_msg*
++dns_msg_deepcopy_region(struct dns_msg* origin, struct regional* region)
++{
++ size_t i;
++ struct dns_msg* res = NULL;
++ res = gen_dns_msg(region, &origin->qinfo, origin->rep->rrset_count);
++ if(!res) return NULL;
++ *res->rep = *origin->rep;
++ for(i=0; i<res->rep->rrset_count; i++) {
++ res->rep->rrsets[i] = packed_rrset_copy_region(
++ origin->rep->rrsets[i], region, 0);
++ if(!res->rep->rrsets[i]) {
++ return NULL;
++ }
++ }
++ return res;
++}
++
+ /** synthesize RRset-only response from cached RRset item */
+ static struct dns_msg*
+ rrset_msg(struct ub_packed_rrset_key* rrset, struct regional* region,
+diff --git a/services/cache/dns.h b/services/cache/dns.h
+index 147f992cb..c2bf23c6d 100644
+--- a/services/cache/dns.h
++++ b/services/cache/dns.h
+@@ -164,6 +164,15 @@ struct dns_msg* tomsg(struct module_env* env, struct query_info* q,
+ struct reply_info* r, struct regional* region, time_t now,
+ int allow_expired, struct regional* scratch);
+
++/**
++ * Deep copy a dns_msg to a region.
++ * @param origin: the dns_msg to copy.
++ * @param region: the region to copy all the data to.
++ * @return the new dns_msg or NULL on malloc error.
++ */
++struct dns_msg* dns_msg_deepcopy_region(struct dns_msg* origin,
++ struct regional* region);
++
+ /**
+ * Find cached message
+ * @param env: module environment with the DNS cache.
+diff --git a/testcode/unitverify.c b/testcode/unitverify.c
+index fb7d84467..395b4c257 100644
+--- a/testcode/unitverify.c
++++ b/testcode/unitverify.c
+@@ -443,9 +443,9 @@ nsec3_hash_test_entry(struct entry* e, rbtree_type* ct,
+
+ ret = nsec3_hash_name(ct, region, buf, nsec3, 0, qname,
+ qinfo.qname_len, &hash);
+- if(ret != 1) {
++ if(ret < 1) {
+ printf("Bad nsec3_hash_name retcode %d\n", ret);
+- unit_assert(ret == 1);
++ unit_assert(ret == 1 || ret == 2);
+ }
+ unit_assert(hash->dname && hash->hash && hash->hash_len &&
+ hash->b32 && hash->b32_len);
+diff --git a/testdata/val_nx_nsec3_collision.rpl b/testdata/val_nx_nsec3_collision.rpl
+index 8ff7e4b06..87a55f565 100644
+--- a/testdata/val_nx_nsec3_collision.rpl
++++ b/testdata/val_nx_nsec3_collision.rpl
+@@ -156,6 +156,9 @@ SECTION QUESTION
+ www.example.com. IN A
+ ENTRY_END
+
++; Allow validation resuming for NSEC3 hash calculations
++STEP 2 TIME_PASSES ELAPSE 0.05
++
+ ; recursion happens here.
+ STEP 10 CHECK_ANSWER
+ ENTRY_BEGIN
+diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c
+index e927e05a6..00b732532 100644
+--- a/util/fptr_wlist.c
++++ b/util/fptr_wlist.c
+@@ -131,7 +131,7 @@ fptr_whitelist_comm_timer(void (*fptr)(void*))
+ else if(fptr == &pending_udp_timer_delay_cb) return 1;
+ else if(fptr == &worker_stat_timer_cb) return 1;
+ else if(fptr == &worker_probe_timer_cb) return 1;
+- else if(fptr == &validate_msg_signatures_timer_cb) return 1;
++ else if(fptr == &validate_suspend_timer_cb) return 1;
+ #ifdef UB_ON_WINDOWS
+ else if(fptr == &wsvc_cron_cb) return 1;
+ #endif
+diff --git a/validator/val_nsec3.c b/validator/val_nsec3.c
+index f4b9b2bca..95d1e4d7e 100644
+--- a/validator/val_nsec3.c
++++ b/validator/val_nsec3.c
+@@ -57,6 +57,19 @@
+ /* we include nsec.h for the bitmap_has_type function */
+ #include "validator/val_nsec.h"
+ #include "sldns/sbuffer.h"
++#include "util/config_file.h"
++
++/**
++ * Max number of NSEC3 calculations at once, suspend query for later.
++ * 8 is low enough and allows for cases where multiple proofs are needed.
++ */
++#define MAX_NSEC3_CALCULATIONS 8
++/**
++ * When all allowed NSEC3 calculations at once resulted in error treat as
++ * bogus. NSEC3 hash errors are not cached and this helps breaks loops with
++ * erroneous data.
++ */
++#define MAX_NSEC3_ERRORS -1
+
+ /**
+ * This function we get from ldns-compat or from base system
+@@ -532,6 +545,17 @@ nsec3_hash_cmp(const void* c1, const void* c2)
+ return memcmp(s1, s2, s1len);
+ }
+
++int
++nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region)
++{
++ if(ct->ct) return 1;
++ ct->ct = (rbtree_type*)regional_alloc(region, sizeof(*ct->ct));
++ if(!ct->ct) return 0;
++ ct->region = region;
++ rbtree_init(ct->ct, &nsec3_hash_cmp);
++ return 1;
++}
++
+ size_t
+ nsec3_get_hashed(sldns_buffer* buf, uint8_t* nm, size_t nmlen, int algo,
+ size_t iter, uint8_t* salt, size_t saltlen, uint8_t* res, size_t max)
+@@ -646,7 +670,7 @@ nsec3_hash_name(rbtree_type* table, struct regional* region, sldns_buffer* buf,
+ c = (struct nsec3_cached_hash*)rbtree_search(table, &looki);
+ if(c) {
+ *hash = c;
+- return 1;
++ return 2;
+ }
+ /* create a new entry */
+ c = (struct nsec3_cached_hash*)regional_alloc(region, sizeof(*c));
+@@ -658,10 +682,10 @@ nsec3_hash_name(rbtree_type* table, struct regional* region, sldns_buffer* buf,
+ c->dname_len = dname_len;
+ r = nsec3_calc_hash(region, buf, c);
+ if(r != 1)
+- return r;
++ return r; /* returns -1 or 0 */
+ r = nsec3_calc_b32(region, buf, c);
+ if(r != 1)
+- return r;
++ return r; /* returns 0 */
+ #ifdef UNBOUND_DEBUG
+ n =
+ #else
+@@ -704,6 +728,7 @@ nsec3_hash_matches_owner(struct nsec3_filter* flt,
+ struct nsec3_cached_hash* hash, struct ub_packed_rrset_key* s)
+ {
+ uint8_t* nm = s->rk.dname;
++ if(!hash) return 0; /* please clang */
+ /* compare, does hash of name based on params in this NSEC3
+ * match the owner name of this NSEC3?
+ * name must be: <hashlength>base32 . zone name
+@@ -730,34 +755,50 @@ nsec3_hash_matches_owner(struct nsec3_filter* flt,
+ * @param nmlen: length of name.
+ * @param rrset: nsec3 that matches is returned here.
+ * @param rr: rr number in nsec3 rrset that matches.
++ * @param calculations: current hash calculations.
+ * @return true if a matching NSEC3 is found, false if not.
+ */
+ static int
+ find_matching_nsec3(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, uint8_t* nm, size_t nmlen,
+- struct ub_packed_rrset_key** rrset, int* rr)
++ struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
++ struct ub_packed_rrset_key** rrset, int* rr,
++ int* calculations)
+ {
+ size_t i_rs;
+ int i_rr;
+ struct ub_packed_rrset_key* s;
+ struct nsec3_cached_hash* hash = NULL;
+ int r;
++ int calc_errors = 0;
+
+ /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
+ for(s=filter_first(flt, &i_rs, &i_rr); s;
+ s=filter_next(flt, &i_rs, &i_rr)) {
++ /* check if we are allowed more calculations */
++ if(*calculations >= MAX_NSEC3_CALCULATIONS) {
++ if(calc_errors == *calculations) {
++ *calculations = MAX_NSEC3_ERRORS;
++ }
++ break;
++ }
+ /* get name hashed for this NSEC3 RR */
+- r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
++ r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
+ s, i_rr, nm, nmlen, &hash);
+ if(r == 0) {
+ log_err("nsec3: malloc failure");
+ break; /* alloc failure */
+- } else if(r != 1)
+- continue; /* malformed NSEC3 */
+- else if(nsec3_hash_matches_owner(flt, hash, s)) {
+- *rrset = s; /* rrset with this name */
+- *rr = i_rr; /* matches hash with these parameters */
+- return 1;
++ } else if(r < 0) {
++ /* malformed NSEC3 */
++ calc_errors++;
++ (*calculations)++;
++ continue;
++ } else {
++ if(r == 1) (*calculations)++;
++ if(nsec3_hash_matches_owner(flt, hash, s)) {
++ *rrset = s; /* rrset with this name */
++ *rr = i_rr; /* matches hash with these parameters */
++ return 1;
++ }
+ }
+ }
+ *rrset = NULL;
+@@ -775,6 +816,7 @@ nsec3_covers(uint8_t* zone, struct nsec3_cached_hash* hash,
+ if(!nsec3_get_nextowner(rrset, rr, &next, &nextlen))
+ return 0; /* malformed RR proves nothing */
+
++ if(!hash) return 0; /* please clang */
+ /* check the owner name is a hashed value . apex
+ * base32 encoded values must have equal length.
+ * hash_value and next hash value must have equal length. */
+@@ -823,35 +865,51 @@ nsec3_covers(uint8_t* zone, struct nsec3_cached_hash* hash,
+ * @param nmlen: length of name.
+ * @param rrset: covering NSEC3 rrset is returned here.
+ * @param rr: rr of cover is returned here.
++ * @param calculations: current hash calculations.
+ * @return true if a covering NSEC3 is found, false if not.
+ */
+ static int
+ find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, uint8_t* nm, size_t nmlen,
+- struct ub_packed_rrset_key** rrset, int* rr)
++ struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
++ struct ub_packed_rrset_key** rrset, int* rr,
++ int* calculations)
+ {
+ size_t i_rs;
+ int i_rr;
+ struct ub_packed_rrset_key* s;
+ struct nsec3_cached_hash* hash = NULL;
+ int r;
++ int calc_errors = 0;
+
+ /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
+ for(s=filter_first(flt, &i_rs, &i_rr); s;
+ s=filter_next(flt, &i_rs, &i_rr)) {
++ /* check if we are allowed more calculations */
++ if(*calculations >= MAX_NSEC3_CALCULATIONS) {
++ if(calc_errors == *calculations) {
++ *calculations = MAX_NSEC3_ERRORS;
++ }
++ break;
++ }
+ /* get name hashed for this NSEC3 RR */
+- r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
++ r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
+ s, i_rr, nm, nmlen, &hash);
+ if(r == 0) {
+ log_err("nsec3: malloc failure");
+ break; /* alloc failure */
+- } else if(r != 1)
+- continue; /* malformed NSEC3 */
+- else if(nsec3_covers(flt->zone, hash, s, i_rr,
+- env->scratch_buffer)) {
+- *rrset = s; /* rrset with this name */
+- *rr = i_rr; /* covers hash with these parameters */
+- return 1;
++ } else if(r < 0) {
++ /* malformed NSEC3 */
++ calc_errors++;
++ (*calculations)++;
++ continue;
++ } else {
++ if(r == 1) (*calculations)++;
++ if(nsec3_covers(flt->zone, hash, s, i_rr,
++ env->scratch_buffer)) {
++ *rrset = s; /* rrset with this name */
++ *rr = i_rr; /* covers hash with these parameters */
++ return 1;
++ }
+ }
+ }
+ *rrset = NULL;
+@@ -869,11 +927,13 @@ find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
+ * @param ct: cached hashes table.
+ * @param qinfo: query that is verified for.
+ * @param ce: closest encloser information is returned in here.
++ * @param calculations: current hash calculations.
+ * @return true if a closest encloser candidate is found, false if not.
+ */
+ static int
+-nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, struct query_info* qinfo, struct ce_response* ce)
++nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
++ struct nsec3_cache_table* ct, struct query_info* qinfo,
++ struct ce_response* ce, int* calculations)
+ {
+ uint8_t* nm = qinfo->qname;
+ size_t nmlen = qinfo->qname_len;
+@@ -888,8 +948,12 @@ nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+ * may be the case. */
+
+ while(dname_subdomain_c(nm, flt->zone)) {
++ if(*calculations >= MAX_NSEC3_CALCULATIONS ||
++ *calculations == MAX_NSEC3_ERRORS) {
++ return 0;
++ }
+ if(find_matching_nsec3(env, flt, ct, nm, nmlen,
+- &ce->ce_rrset, &ce->ce_rr)) {
++ &ce->ce_rrset, &ce->ce_rr, calculations)) {
+ ce->ce = nm;
+ ce->ce_len = nmlen;
+ return 1;
+@@ -933,22 +997,38 @@ next_closer(uint8_t* qname, size_t qnamelen, uint8_t* ce,
+ * If set true, and the return value is true, then you can be
+ * certain that the ce.nc_rrset and ce.nc_rr are set properly.
+ * @param ce: closest encloser information is returned in here.
++ * @param calculations: pointer to the current NSEC3 hash calculations.
+ * @return bogus if no closest encloser could be proven.
+ * secure if a closest encloser could be proven, ce is set.
+ * insecure if the closest-encloser candidate turns out to prove
+ * that an insecure delegation exists above the qname.
++ * unchecked if no more hash calculations are allowed at this point.
+ */
+ static enum sec_status
+-nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, struct query_info* qinfo, int prove_does_not_exist,
+- struct ce_response* ce)
++nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
++ struct nsec3_cache_table* ct, struct query_info* qinfo,
++ int prove_does_not_exist, struct ce_response* ce, int* calculations)
+ {
+ uint8_t* nc;
+ size_t nc_len;
+ /* robust: clean out ce, in case it gets abused later */
+ memset(ce, 0, sizeof(*ce));
+
+- if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce)) {
++ if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce, calculations)) {
++ if(*calculations == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
++ "not find a candidate for the closest "
++ "encloser; all attempted hash calculations "
++ "were erroneous; bogus");
++ return sec_status_bogus;
++ } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
++ "not find a candidate for the closest "
++ "encloser; reached MAX_NSEC3_CALCULATIONS "
++ "(%d); unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+ verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
+ "not find a candidate for the closest encloser.");
+ return sec_status_bogus;
+@@ -989,9 +1069,23 @@ nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+ /* Otherwise, we need to show that the next closer name is covered. */
+ next_closer(qinfo->qname, qinfo->qname_len, ce->ce, &nc, &nc_len);
+ if(!find_covering_nsec3(env, flt, ct, nc, nc_len,
+- &ce->nc_rrset, &ce->nc_rr)) {
++ &ce->nc_rrset, &ce->nc_rr, calculations)) {
++ if(*calculations == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "nsec3: Could not find proof that the "
++ "candidate encloser was the closest encloser; "
++ "all attempted hash calculations were "
++ "erroneous; bogus");
++ return sec_status_bogus;
++ } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "nsec3: Could not find proof that the "
++ "candidate encloser was the closest encloser; "
++ "reached MAX_NSEC3_CALCULATIONS (%d); "
++ "unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+ verbose(VERB_ALGO, "nsec3: Could not find proof that the "
+- "candidate encloser was the closest encloser");
++ "candidate encloser was the closest encloser");
+ return sec_status_bogus;
+ }
+ return sec_status_secure;
+@@ -1019,8 +1113,8 @@ nsec3_ce_wildcard(struct regional* region, uint8_t* ce, size_t celen,
+
+ /** Do the name error proof */
+ static enum sec_status
+-nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, struct query_info* qinfo)
++nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
++ struct nsec3_cache_table* ct, struct query_info* qinfo, int* calc)
+ {
+ struct ce_response ce;
+ uint8_t* wc;
+@@ -1032,11 +1126,15 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+ /* First locate and prove the closest encloser to qname. We will
+ * use the variant that fails if the closest encloser turns out
+ * to be qname. */
+- sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
++ sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
+ if(sec != sec_status_secure) {
+ if(sec == sec_status_bogus)
+ verbose(VERB_ALGO, "nsec3 nameerror proof: failed "
+ "to prove a closest encloser");
++ else if(sec == sec_status_unchecked)
++ verbose(VERB_ALGO, "nsec3 nameerror proof: will "
++ "continue proving closest encloser after "
++ "suspend");
+ else verbose(VERB_ALGO, "nsec3 nameerror proof: closest "
+ "nsec3 is an insecure delegation");
+ return sec;
+@@ -1046,9 +1144,27 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+ /* At this point, we know that qname does not exist. Now we need
+ * to prove that the wildcard does not exist. */
+ log_assert(ce.ce);
+- wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
+- if(!wc || !find_covering_nsec3(env, flt, ct, wc, wclen,
+- &wc_rrset, &wc_rr)) {
++ wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
++ if(!wc) {
++ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
++ "that the applicable wildcard did not exist.");
++ return sec_status_bogus;
++ }
++ if(!find_covering_nsec3(env, flt, ct, wc, wclen, &wc_rrset, &wc_rr, calc)) {
++ if(*calc == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
++ "that the applicable wildcard did not exist; "
++ "all attempted hash calculations were "
++ "erroneous; bogus");
++ return sec_status_bogus;
++ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
++ "that the applicable wildcard did not exist; "
++ "reached MAX_NSEC3_CALCULATIONS (%d); "
++ "unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+ verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+ "that the applicable wildcard did not exist.");
+ return sec_status_bogus;
+@@ -1064,14 +1180,13 @@ nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+ enum sec_status
+ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey)
++ struct query_info* qinfo, struct key_entry_key* kkey,
++ struct nsec3_cache_table* ct, int* calc)
+ {
+- rbtree_type ct;
+ struct nsec3_filter flt;
+
+ if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
+ return sec_status_bogus; /* no valid NSEC3s, bogus */
+- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
+ filter_init(&flt, list, num, qinfo); /* init RR iterator */
+ if(!flt.zone)
+ return sec_status_bogus; /* no RRs */
+@@ -1079,7 +1194,7 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
+ return sec_status_insecure; /* iteration count too high */
+ log_nametypeclass(VERB_ALGO, "start nsec3 nameerror proof, zone",
+ flt.zone, 0, 0);
+- return nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
++ return nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
+ }
+
+ /*
+@@ -1089,8 +1204,9 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
+
+ /** Do the nodata proof */
+ static enum sec_status
+-nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+- rbtree_type* ct, struct query_info* qinfo)
++nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
++ struct nsec3_cache_table* ct, struct query_info* qinfo,
++ int* calc)
+ {
+ struct ce_response ce;
+ uint8_t* wc;
+@@ -1100,7 +1216,7 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+ enum sec_status sec;
+
+ if(find_matching_nsec3(env, flt, ct, qinfo->qname, qinfo->qname_len,
+- &rrset, &rr)) {
++ &rrset, &rr, calc)) {
+ /* cases 1 and 2 */
+ if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
+ verbose(VERB_ALGO, "proveNodata: Matching NSEC3 "
+@@ -1144,11 +1260,23 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+ }
+ return sec_status_secure;
+ }
++ if(*calc == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "proveNodata: all attempted hash "
++ "calculations were erroneous while finding a matching "
++ "NSEC3, bogus");
++ return sec_status_bogus;
++ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "proveNodata: reached "
++ "MAX_NSEC3_CALCULATIONS (%d) while finding a "
++ "matching NSEC3; unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+
+ /* For cases 3 - 5, we need the proven closest encloser, and it
+ * can't match qname. Although, at this point, we know that it
+ * won't since we just checked that. */
+- sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
++ sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
+ if(sec == sec_status_bogus) {
+ verbose(VERB_ALGO, "proveNodata: did not match qname, "
+ "nor found a proven closest encloser.");
+@@ -1157,14 +1285,17 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+ verbose(VERB_ALGO, "proveNodata: closest nsec3 is insecure "
+ "delegation.");
+ return sec_status_insecure;
++ } else if(sec==sec_status_unchecked) {
++ return sec_status_unchecked;
+ }
+
+ /* Case 3: removed */
+
+ /* Case 4: */
+ log_assert(ce.ce);
+- wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
+- if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr)) {
++ wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
++ if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr,
++ calc)) {
+ /* found wildcard */
+ if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
+ verbose(VERB_ALGO, "nsec3 nodata proof: matching "
+@@ -1195,6 +1326,18 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+ }
+ return sec_status_secure;
+ }
++ if(*calc == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "nsec3 nodata proof: all attempted hash "
++ "calculations were erroneous while matching "
++ "wildcard, bogus");
++ return sec_status_bogus;
++ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "nsec3 nodata proof: reached "
++ "MAX_NSEC3_CALCULATIONS (%d) while matching "
++ "wildcard, unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+
+ /* Case 5: */
+ /* Due to forwarders, cnames, and other collating effects, we
+@@ -1223,28 +1366,27 @@ nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+ enum sec_status
+ nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey)
++ struct query_info* qinfo, struct key_entry_key* kkey,
++ struct nsec3_cache_table* ct, int* calc)
+ {
+- rbtree_type ct;
+ struct nsec3_filter flt;
+
+ if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
+ return sec_status_bogus; /* no valid NSEC3s, bogus */
+- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
+ filter_init(&flt, list, num, qinfo); /* init RR iterator */
+ if(!flt.zone)
+ return sec_status_bogus; /* no RRs */
+ if(nsec3_iteration_count_high(ve, &flt, kkey))
+ return sec_status_insecure; /* iteration count too high */
+- return nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
++ return nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
+ }
+
+ enum sec_status
+ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc)
++ struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
++ struct nsec3_cache_table* ct, int* calc)
+ {
+- rbtree_type ct;
+ struct nsec3_filter flt;
+ struct ce_response ce;
+ uint8_t* nc;
+@@ -1254,7 +1396,6 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
+
+ if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
+ return sec_status_bogus; /* no valid NSEC3s, bogus */
+- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
+ filter_init(&flt, list, num, qinfo); /* init RR iterator */
+ if(!flt.zone)
+ return sec_status_bogus; /* no RRs */
+@@ -1272,8 +1413,22 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
+ /* Now we still need to prove that the original data did not exist.
+ * Otherwise, we need to show that the next closer name is covered. */
+ next_closer(qinfo->qname, qinfo->qname_len, ce.ce, &nc, &nc_len);
+- if(!find_covering_nsec3(env, &flt, &ct, nc, nc_len,
+- &ce.nc_rrset, &ce.nc_rr)) {
++ if(!find_covering_nsec3(env, &flt, ct, nc, nc_len,
++ &ce.nc_rrset, &ce.nc_rr, calc)) {
++ if(*calc == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "proveWildcard: did not find a "
++ "covering NSEC3 that covered the next closer "
++ "name; all attempted hash calculations were "
++ "erroneous; bogus");
++ return sec_status_bogus;
++ } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "proveWildcard: did not find a "
++ "covering NSEC3 that covered the next closer "
++ "name; reached MAX_NSEC3_CALCULATIONS "
++ "(%d); unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+ verbose(VERB_ALGO, "proveWildcard: did not find a covering "
+ "NSEC3 that covered the next closer name.");
+ return sec_status_bogus;
+@@ -1320,13 +1475,16 @@ enum sec_status
+ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+ struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
+- sldns_ede_code* reason_bogus, struct module_qstate* qstate)
++ sldns_ede_code* reason_bogus, struct module_qstate* qstate,
++ struct nsec3_cache_table* ct)
+ {
+- rbtree_type ct;
+ struct nsec3_filter flt;
+ struct ce_response ce;
+ struct ub_packed_rrset_key* rrset;
+ int rr;
++ int calc = 0;
++ enum sec_status sec;
++
+ log_assert(qinfo->qtype == LDNS_RR_TYPE_DS);
+
+ if(!list || num == 0 || !kkey || !key_entry_isgood(kkey)) {
+@@ -1337,7 +1495,6 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+ *reason = "not all NSEC3 records secure";
+ return sec_status_bogus; /* not all NSEC3 records secure */
+ }
+- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
+ filter_init(&flt, list, num, qinfo); /* init RR iterator */
+ if(!flt.zone) {
+ *reason = "no NSEC3 records";
+@@ -1348,8 +1505,8 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+
+ /* Look for a matching NSEC3 to qname -- this is the normal
+ * NODATA case. */
+- if(find_matching_nsec3(env, &flt, &ct, qinfo->qname, qinfo->qname_len,
+- &rrset, &rr)) {
++ if(find_matching_nsec3(env, &flt, ct, qinfo->qname, qinfo->qname_len,
++ &rrset, &rr, &calc)) {
+ /* If the matching NSEC3 has the SOA bit set, it is from
+ * the wrong zone (the child instead of the parent). If
+ * it has the DS bit set, then we were lied to. */
+@@ -1372,10 +1529,24 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+ /* Otherwise, this proves no DS. */
+ return sec_status_secure;
+ }
++ if(calc == MAX_NSEC3_ERRORS) {
++ verbose(VERB_ALGO, "nsec3 provenods: all attempted hash "
++ "calculations were erroneous while finding a matching "
++ "NSEC3, bogus");
++ return sec_status_bogus;
++ } else if(calc >= MAX_NSEC3_CALCULATIONS) {
++ verbose(VERB_ALGO, "nsec3 provenods: reached "
++ "MAX_NSEC3_CALCULATIONS (%d) while finding a "
++ "matching NSEC3, unchecked still",
++ MAX_NSEC3_CALCULATIONS);
++ return sec_status_unchecked;
++ }
+
+ /* Otherwise, we are probably in the opt-out case. */
+- if(nsec3_prove_closest_encloser(env, &flt, &ct, qinfo, 1, &ce)
+- != sec_status_secure) {
++ sec = nsec3_prove_closest_encloser(env, &flt, ct, qinfo, 1, &ce, &calc);
++ if(sec == sec_status_unchecked) {
++ return sec_status_unchecked;
++ } else if(sec != sec_status_secure) {
+ /* an insecure delegation *above* the qname does not prove
+ * anything about this qname exactly, and bogus is bogus */
+ verbose(VERB_ALGO, "nsec3 provenods: did not match qname, "
+@@ -1409,17 +1580,16 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+
+ enum sec_status
+ nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
+- struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey, int* nodata)
++ struct ub_packed_rrset_key** list, size_t num,
++ struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
++ struct nsec3_cache_table* ct, int* calc)
+ {
+ enum sec_status sec, secnx;
+- rbtree_type ct;
+ struct nsec3_filter flt;
+ *nodata = 0;
+
+ if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
+ return sec_status_bogus; /* no valid NSEC3s, bogus */
+- rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
+ filter_init(&flt, list, num, qinfo); /* init RR iterator */
+ if(!flt.zone)
+ return sec_status_bogus; /* no RRs */
+@@ -1429,16 +1599,20 @@ nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
+ /* try nxdomain and nodata after another, while keeping the
+ * hash cache intact */
+
+- secnx = nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
++ secnx = nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
+ if(secnx==sec_status_secure)
+ return sec_status_secure;
+- sec = nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
++ else if(secnx == sec_status_unchecked)
++ return sec_status_unchecked;
++ sec = nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
+ if(sec==sec_status_secure) {
+ *nodata = 1;
+ } else if(sec == sec_status_insecure) {
+ *nodata = 1;
+ } else if(secnx == sec_status_insecure) {
+ sec = sec_status_insecure;
++ } else if(sec == sec_status_unchecked) {
++ return sec_status_unchecked;
+ }
+ return sec;
+ }
+diff --git a/validator/val_nsec3.h b/validator/val_nsec3.h
+index 7676fc8b2..8ca912934 100644
+--- a/validator/val_nsec3.h
++++ b/validator/val_nsec3.h
+@@ -98,6 +98,15 @@ struct sldns_buffer;
+ /** The SHA1 hash algorithm for NSEC3 */
+ #define NSEC3_HASH_SHA1 0x01
+
++/**
++* Cache table for NSEC3 hashes.
++* It keeps a *pointer* to the region its items are allocated.
++*/
++struct nsec3_cache_table {
++ rbtree_type* ct;
++ struct regional* region;
++};
++
+ /**
+ * Determine if the set of NSEC3 records provided with a response prove NAME
+ * ERROR. This means that the NSEC3s prove a) the closest encloser exists,
+@@ -110,14 +119,18 @@ struct sldns_buffer;
+ * @param num: number of RRsets in the array to examine.
+ * @param qinfo: query that is verified for.
+ * @param kkey: key entry that signed the NSEC3s.
++ * @param ct: cached hashes table.
++ * @param calc: current hash calculations.
+ * @return:
+ * sec_status SECURE of the Name Error is proven by the NSEC3 RRs,
+- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
++ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
++ * UNCHECKED if no more hash calculations are allowed at this point.
+ */
+ enum sec_status
+ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey);
++ struct query_info* qinfo, struct key_entry_key* kkey,
++ struct nsec3_cache_table* ct, int* calc);
+
+ /**
+ * Determine if the NSEC3s provided in a response prove the NOERROR/NODATA
+@@ -144,15 +157,18 @@ nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
+ * @param num: number of RRsets in the array to examine.
+ * @param qinfo: query that is verified for.
+ * @param kkey: key entry that signed the NSEC3s.
++ * @param ct: cached hashes table.
++ * @param calc: current hash calculations.
+ * @return:
+ * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
+- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
++ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
++ * UNCHECKED if no more hash calculations are allowed at this point.
+ */
+ enum sec_status
+ nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey);
+-
++ struct query_info* qinfo, struct key_entry_key* kkey,
++ struct nsec3_cache_table* ct, int* calc);
+
+ /**
+ * Prove that a positive wildcard match was appropriate (no direct match
+@@ -166,14 +182,18 @@ nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
+ * @param kkey: key entry that signed the NSEC3s.
+ * @param wc: The purported wildcard that matched. This is the wildcard name
+ * as *.wildcard.name., with the *. label already removed.
++ * @param ct: cached hashes table.
++ * @param calc: current hash calculations.
+ * @return:
+ * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
+- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
++ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
++ * UNCHECKED if no more hash calculations are allowed at this point.
+ */
+ enum sec_status
+ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc);
++ struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
++ struct nsec3_cache_table* ct, int* calc);
+
+ /**
+ * Prove that a DS response either had no DS, or wasn't a delegation point.
+@@ -189,17 +209,20 @@ nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
+ * @param reason: string for bogus result.
+ * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
+ * @param qstate: qstate with region.
++ * @param ct: cached hashes table.
+ * @return:
+ * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
+ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ * or if there was no DS in an insecure (i.e., opt-in) way,
+- * INDETERMINATE if it was clear that this wasn't a delegation point.
++ * INDETERMINATE if it was clear that this wasn't a delegation point,
++ * UNCHECKED if no more hash calculations are allowed at this point.
+ */
+ enum sec_status
+ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+ struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
+- sldns_ede_code* reason_bogus, struct module_qstate* qstate);
++ sldns_ede_code* reason_bogus, struct module_qstate* qstate,
++ struct nsec3_cache_table* ct);
+
+ /**
+ * Prove NXDOMAIN or NODATA.
+@@ -212,14 +235,18 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve,
+ * @param kkey: key entry that signed the NSEC3s.
+ * @param nodata: if return value is secure, this indicates if nodata or
+ * nxdomain was proven.
++ * @param ct: cached hashes table.
++ * @param calc: current hash calculations.
+ * @return:
+ * sec_status SECURE of the proposition is proven by the NSEC3 RRs,
+- * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
++ * BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
++ * UNCHECKED if no more hash calculations are allowed at this point.
+ */
+ enum sec_status
+ nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
+ struct ub_packed_rrset_key** list, size_t num,
+- struct query_info* qinfo, struct key_entry_key* kkey, int* nodata);
++ struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
++ struct nsec3_cache_table* ct, int* calc);
+
+ /**
+ * The NSEC3 hash result storage.
+@@ -256,6 +283,14 @@ struct nsec3_cached_hash {
+ */
+ int nsec3_hash_cmp(const void* c1, const void* c2);
+
++/**
++ * Initialise the NSEC3 cache table.
++ * @param ct: the nsec3 cache table.
++ * @param region: the region where allocations for the table will happen.
++ * @return true on success, false on malloc error.
++ */
++int nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region);
++
+ /**
+ * Obtain the hash of an owner name.
+ * Used internally by the nsec3 proof functions in this file.
+@@ -272,7 +307,8 @@ int nsec3_hash_cmp(const void* c1, const void* c2);
+ * @param dname_len: the length of the name.
+ * @param hash: the hash node is returned on success.
+ * @return:
+- * 1 on success, either from cache or newly hashed hash is returned.
++ * 2 on success, hash from cache is returned.
++ * 1 on success, newly computed hash is returned.
+ * 0 on a malloc failure.
+ * -1 if the NSEC3 rr was badly formatted (i.e. formerr).
+ */
+diff --git a/validator/validator.c b/validator/validator.c
+index a4549c00b..01303a844 100644
+--- a/validator/validator.c
++++ b/validator/validator.c
+@@ -72,7 +72,7 @@
+ /* forward decl for cache response and normal super inform calls of a DS */
+ static void process_ds_response(struct module_qstate* qstate,
+ struct val_qstate* vq, int id, int rcode, struct dns_msg* msg,
+- struct query_info* qinfo, struct sock_list* origin);
++ struct query_info* qinfo, struct sock_list* origin, int* suspend);
+
+
+ /* Updates the suplied EDE (RFC8914) code selectively so we don't loose
+@@ -293,10 +293,10 @@ val_restart(struct val_qstate* vq)
+ struct comm_timer* temp_timer;
+ int restart_count;
+ if(!vq) return;
+- temp_timer = vq->msg_signatures_timer;
++ temp_timer = vq->suspend_timer;
+ restart_count = vq->restart_count+1;
+ memset(vq, 0, sizeof(*vq));
+- vq->msg_signatures_timer = temp_timer;
++ vq->suspend_timer = temp_timer;
+ vq->restart_count = restart_count;
+ vq->state = VAL_INIT_STATE;
+ }
+@@ -614,7 +614,7 @@ prime_trust_anchor(struct module_qstate* qstate, struct val_qstate* vq,
+ * @param chase_reply: answer to validate.
+ * @param key_entry: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
+- * @param suspend: returned true if the task takes to long and needs to
++ * @param suspend: returned true if the task takes too long and needs to
+ * suspend to continue the effort later.
+ * @return false if any of the rrsets in the an or ns sections of the message
+ * fail to verify. The message is then set to bogus.
+@@ -778,37 +778,38 @@ validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq,
+ }
+
+ void
+-validate_msg_signatures_timer_cb(void* arg)
++validate_suspend_timer_cb(void* arg)
+ {
+ struct module_qstate* qstate = (struct module_qstate*)arg;
+- verbose(VERB_ALGO, "validate_msg_signatures timer, continue");
++ verbose(VERB_ALGO, "validate_suspend timer, continue");
+ mesh_run(qstate->env->mesh, qstate->mesh_info, module_event_pass,
+ NULL);
+ }
+
+ /** Setup timer to continue validation of msg signatures later */
+ static int
+-validate_msg_signatures_setup_timer(struct module_qstate* qstate,
+- struct val_qstate* vq, int id)
++validate_suspend_setup_timer(struct module_qstate* qstate,
++ struct val_qstate* vq, int id, enum val_state resume_state)
+ {
+ struct timeval tv;
+ int usec, slack, base;
+ if(vq->suspend_count >= MAX_VALIDATION_SUSPENDS) {
+- verbose(VERB_ALGO, "validate_msg_signatures_setup_timer: "
++ verbose(VERB_ALGO, "validate_suspend timer: "
+ "reached MAX_VALIDATION_SUSPENDS (%d); error out",
+ MAX_VALIDATION_SUSPENDS);
+ errinf(qstate, "max validation suspends reached, "
+ "too many RRSIG validations");
+ return 0;
+ }
+- vq->state = VAL_VALIDATE_STATE;
++ verbose(VERB_ALGO, "validate_suspend timer, set for suspend");
++ vq->state = resume_state;
+ qstate->ext_state[id] = module_wait_reply;
+- if(!vq->msg_signatures_timer) {
+- vq->msg_signatures_timer = comm_timer_create(
++ if(!vq->suspend_timer) {
++ vq->suspend_timer = comm_timer_create(
+ qstate->env->worker_base,
+- validate_msg_signatures_timer_cb, qstate);
+- if(!vq->msg_signatures_timer) {
+- log_err("validate_msg_signatures_setup_timer: "
++ validate_suspend_timer_cb, qstate);
++ if(!vq->suspend_timer) {
++ log_err("validate_suspend_setup_timer: "
+ "out of memory for comm_timer_create");
+ return 0;
+ }
+@@ -839,7 +840,7 @@ validate_msg_signatures_setup_timer(struct module_qstate* qstate,
+ tv.tv_usec = (usec % 1000000);
+ tv.tv_sec = (usec / 1000000);
+ vq->suspend_count ++;
+- comm_timer_set(vq->msg_signatures_timer, &tv);
++ comm_timer_set(vq->suspend_timer, &tv);
+ return 1;
+ }
+
+@@ -941,11 +942,17 @@ remove_spurious_authority(struct reply_info* chase_reply,
+ * @param chase_reply: answer to that query to validate.
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_positive_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey)
++ struct key_entry_key* kkey, struct module_qstate* qstate,
++ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
+ {
+ uint8_t* wc = NULL;
+ size_t wl;
+@@ -954,6 +961,7 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
+ int nsec3s_seen = 0;
+ size_t i;
+ struct ub_packed_rrset_key* s;
++ *suspend = 0;
+
+ /* validate the ANSWER section - this will be the answer itself */
+ for(i=0; i<chase_reply->an_numrrsets; i++) {
+@@ -1005,17 +1013,23 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
+ /* If this was a positive wildcard response that we haven't already
+ * proven, and we have NSEC3 records, try to prove it using the NSEC3
+ * records. */
+- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
+- enum sec_status sec = nsec3_prove_wildcard(env, ve,
++ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
++ enum sec_status sec = nsec3_prove_wildcard(env, ve,
+ chase_reply->rrsets+chase_reply->an_numrrsets,
+- chase_reply->ns_numrrsets, qchase, kkey, wc);
++ chase_reply->ns_numrrsets, qchase, kkey, wc,
++ &vq->nsec3_cache_table, nsec3_calculations);
+ if(sec == sec_status_insecure) {
+ verbose(VERB_ALGO, "Positive wildcard response is "
+ "insecure");
+ chase_reply->security = sec_status_insecure;
+ return;
+- } else if(sec == sec_status_secure)
++ } else if(sec == sec_status_secure) {
+ wc_NSEC_ok = 1;
++ } else if(sec == sec_status_unchecked) {
++ *suspend = 1;
++ return;
++ }
+ }
+
+ /* If after all this, we still haven't proven the positive wildcard
+@@ -1047,11 +1061,17 @@ validate_positive_response(struct module_env* env, struct val_env* ve,
+ * @param chase_reply: answer to that query to validate.
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_nodata_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey)
++ struct key_entry_key* kkey, struct module_qstate* qstate,
++ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
+ {
+ /* Since we are here, there must be nothing in the ANSWER section to
+ * validate. */
+@@ -1068,6 +1088,7 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
+ int nsec3s_seen = 0; /* nsec3s seen */
+ struct ub_packed_rrset_key* s;
+ size_t i;
++ *suspend = 0;
+
+ for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
+ chase_reply->ns_numrrsets; i++) {
+@@ -1106,16 +1127,23 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
+ }
+ }
+
+- if(!has_valid_nsec && nsec3s_seen) {
++ if(!has_valid_nsec && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+ enum sec_status sec = nsec3_prove_nodata(env, ve,
+ chase_reply->rrsets+chase_reply->an_numrrsets,
+- chase_reply->ns_numrrsets, qchase, kkey);
++ chase_reply->ns_numrrsets, qchase, kkey,
++ &vq->nsec3_cache_table, nsec3_calculations);
+ if(sec == sec_status_insecure) {
+ verbose(VERB_ALGO, "NODATA response is insecure");
+ chase_reply->security = sec_status_insecure;
+ return;
+- } else if(sec == sec_status_secure)
++ } else if(sec == sec_status_secure) {
+ has_valid_nsec = 1;
++ } else if(sec == sec_status_unchecked) {
++ /* check is incomplete; suspend */
++ *suspend = 1;
++ return;
++ }
+ }
+
+ if(!has_valid_nsec) {
+@@ -1147,11 +1175,18 @@ validate_nodata_response(struct module_env* env, struct val_env* ve,
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
+ * @param rcode: adjusted RCODE, in case of RCODE/proof mismatch leniency.
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_nameerror_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey, int* rcode)
++ struct key_entry_key* kkey, int* rcode,
++ struct module_qstate* qstate, struct val_qstate* vq,
++ int* nsec3_calculations, int* suspend)
+ {
+ int has_valid_nsec = 0;
+ int has_valid_wnsec = 0;
+@@ -1161,6 +1196,7 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
+ uint8_t* ce;
+ int ce_labs = 0;
+ int prev_ce_labs = 0;
++ *suspend = 0;
+
+ for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
+ chase_reply->ns_numrrsets; i++) {
+@@ -1190,13 +1226,18 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
+ nsec3s_seen = 1;
+ }
+
+- if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen) {
++ if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+ /* use NSEC3 proof, both answer and auth rrsets, in case
+ * NSEC3s end up in the answer (due to qtype=NSEC3 or so) */
+ chase_reply->security = nsec3_prove_nameerror(env, ve,
+ chase_reply->rrsets, chase_reply->an_numrrsets+
+- chase_reply->ns_numrrsets, qchase, kkey);
+- if(chase_reply->security != sec_status_secure) {
++ chase_reply->ns_numrrsets, qchase, kkey,
++ &vq->nsec3_cache_table, nsec3_calculations);
++ if(chase_reply->security == sec_status_unchecked) {
++ *suspend = 1;
++ return;
++ } else if(chase_reply->security != sec_status_secure) {
+ verbose(VERB_QUERY, "NameError response failed nsec, "
+ "nsec3 proof was %s", sec_status_to_string(
+ chase_reply->security));
+@@ -1208,26 +1249,34 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve,
+
+ /* If the message fails to prove either condition, it is bogus. */
+ if(!has_valid_nsec) {
++ validate_nodata_response(env, ve, qchase, chase_reply, kkey,
++ qstate, vq, nsec3_calculations, suspend);
++ if(*suspend) return;
+ verbose(VERB_QUERY, "NameError response has failed to prove: "
+ "qname does not exist");
+- chase_reply->security = sec_status_bogus;
+- update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+ /* Be lenient with RCODE in NSEC NameError responses */
+- validate_nodata_response(env, ve, qchase, chase_reply, kkey);
+- if (chase_reply->security == sec_status_secure)
++ if(chase_reply->security == sec_status_secure) {
+ *rcode = LDNS_RCODE_NOERROR;
++ } else {
++ chase_reply->security = sec_status_bogus;
++ update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
++ }
+ return;
+ }
+
+ if(!has_valid_wnsec) {
++ validate_nodata_response(env, ve, qchase, chase_reply, kkey,
++ qstate, vq, nsec3_calculations, suspend);
++ if(*suspend) return;
+ verbose(VERB_QUERY, "NameError response has failed to prove: "
+ "covering wildcard does not exist");
+- chase_reply->security = sec_status_bogus;
+- update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+ /* Be lenient with RCODE in NSEC NameError responses */
+- validate_nodata_response(env, ve, qchase, chase_reply, kkey);
+- if (chase_reply->security == sec_status_secure)
++ if (chase_reply->security == sec_status_secure) {
+ *rcode = LDNS_RCODE_NOERROR;
++ } else {
++ chase_reply->security = sec_status_bogus;
++ update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
++ }
+ return;
+ }
+
+@@ -1287,11 +1336,17 @@ validate_referral_response(struct reply_info* chase_reply)
+ * @param chase_reply: answer to that query to validate.
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_any_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey)
++ struct key_entry_key* kkey, struct module_qstate* qstate,
++ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
+ {
+ /* all answer and auth rrsets already verified */
+ /* but check if a wildcard response is given, then check NSEC/NSEC3
+@@ -1302,6 +1357,7 @@ validate_any_response(struct module_env* env, struct val_env* ve,
+ int nsec3s_seen = 0;
+ size_t i;
+ struct ub_packed_rrset_key* s;
++ *suspend = 0;
+
+ if(qchase->qtype != LDNS_RR_TYPE_ANY) {
+ log_err("internal error: ANY validation called for non-ANY");
+@@ -1356,19 +1412,25 @@ validate_any_response(struct module_env* env, struct val_env* ve,
+ /* If this was a positive wildcard response that we haven't already
+ * proven, and we have NSEC3 records, try to prove it using the NSEC3
+ * records. */
+- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
++ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+ /* look both in answer and auth section for NSEC3s */
+- enum sec_status sec = nsec3_prove_wildcard(env, ve,
++ enum sec_status sec = nsec3_prove_wildcard(env, ve,
+ chase_reply->rrsets,
+- chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
+- qchase, kkey, wc);
++ chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
++ qchase, kkey, wc, &vq->nsec3_cache_table,
++ nsec3_calculations);
+ if(sec == sec_status_insecure) {
+ verbose(VERB_ALGO, "Positive ANY wildcard response is "
+ "insecure");
+ chase_reply->security = sec_status_insecure;
+ return;
+- } else if(sec == sec_status_secure)
++ } else if(sec == sec_status_secure) {
+ wc_NSEC_ok = 1;
++ } else if(sec == sec_status_unchecked) {
++ *suspend = 1;
++ return;
++ }
+ }
+
+ /* If after all this, we still haven't proven the positive wildcard
+@@ -1401,11 +1463,17 @@ validate_any_response(struct module_env* env, struct val_env* ve,
+ * @param chase_reply: answer to that query to validate.
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_cname_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey)
++ struct key_entry_key* kkey, struct module_qstate* qstate,
++ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
+ {
+ uint8_t* wc = NULL;
+ size_t wl;
+@@ -1413,6 +1481,7 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
+ int nsec3s_seen = 0;
+ size_t i;
+ struct ub_packed_rrset_key* s;
++ *suspend = 0;
+
+ /* validate the ANSWER section - this will be the CNAME (+DNAME) */
+ for(i=0; i<chase_reply->an_numrrsets; i++) {
+@@ -1477,17 +1546,23 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
+ /* If this was a positive wildcard response that we haven't already
+ * proven, and we have NSEC3 records, try to prove it using the NSEC3
+ * records. */
+- if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
+- enum sec_status sec = nsec3_prove_wildcard(env, ve,
++ if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
++ enum sec_status sec = nsec3_prove_wildcard(env, ve,
+ chase_reply->rrsets+chase_reply->an_numrrsets,
+- chase_reply->ns_numrrsets, qchase, kkey, wc);
++ chase_reply->ns_numrrsets, qchase, kkey, wc,
++ &vq->nsec3_cache_table, nsec3_calculations);
+ if(sec == sec_status_insecure) {
+ verbose(VERB_ALGO, "wildcard CNAME response is "
+ "insecure");
+ chase_reply->security = sec_status_insecure;
+ return;
+- } else if(sec == sec_status_secure)
++ } else if(sec == sec_status_secure) {
+ wc_NSEC_ok = 1;
++ } else if(sec == sec_status_unchecked) {
++ *suspend = 1;
++ return;
++ }
+ }
+
+ /* If after all this, we still haven't proven the positive wildcard
+@@ -1518,11 +1593,17 @@ validate_cname_response(struct module_env* env, struct val_env* ve,
+ * @param chase_reply: answer to that query to validate.
+ * @param kkey: the key entry, which is trusted, and which matches
+ * the signer of the answer. The key entry isgood().
++ * @param qstate: query state for the region.
++ * @param vq: validator state for the nsec3 cache table.
++ * @param nsec3_calculations: current nsec3 hash calculations.
++ * @param suspend: returned true if the task takes too long and needs to
++ * suspend to continue the effort later.
+ */
+ static void
+ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
+ struct query_info* qchase, struct reply_info* chase_reply,
+- struct key_entry_key* kkey)
++ struct key_entry_key* kkey, struct module_qstate* qstate,
++ struct val_qstate* vq, int* nsec3_calculations, int* suspend)
+ {
+ int nodata_valid_nsec = 0; /* If true, then NODATA has been proven.*/
+ uint8_t* ce = NULL; /* for wildcard nodata responses. This is the
+@@ -1536,6 +1617,7 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
+ uint8_t* nsec_ce; /* Used to find the NSEC with the longest ce */
+ int ce_labs = 0;
+ int prev_ce_labs = 0;
++ *suspend = 0;
+
+ /* the AUTHORITY section */
+ for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
+@@ -1601,11 +1683,13 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
+ update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+ return;
+ }
+- if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen) {
++ if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen &&
++ nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+ int nodata;
+ enum sec_status sec = nsec3_prove_nxornodata(env, ve,
+ chase_reply->rrsets+chase_reply->an_numrrsets,
+- chase_reply->ns_numrrsets, qchase, kkey, &nodata);
++ chase_reply->ns_numrrsets, qchase, kkey, &nodata,
++ &vq->nsec3_cache_table, nsec3_calculations);
+ if(sec == sec_status_insecure) {
+ verbose(VERB_ALGO, "CNAMEchain to noanswer response "
+ "is insecure");
+@@ -1615,6 +1699,9 @@ validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
+ if(nodata)
+ nodata_valid_nsec = 1;
+ else nxdomain_valid_nsec = 1;
++ } else if(sec == sec_status_unchecked) {
++ *suspend = 1;
++ return;
+ }
+ }
+
+@@ -1965,13 +2052,37 @@ processFindKey(struct module_qstate* qstate, struct val_qstate* vq, int id)
+ * Uses negative cache for NSEC3 lookup of DS responses. */
+ /* only if cache not blacklisted, of course */
+ struct dns_msg* msg;
+- if(!qstate->blacklist && !vq->chain_blacklist &&
++ int suspend;
++ if(vq->sub_ds_msg) {
++ /* We have a suspended DS reply from a sub-query;
++ * process it. */
++ verbose(VERB_ALGO, "Process suspended sub DS response");
++ msg = vq->sub_ds_msg;
++ process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
++ msg, &msg->qinfo, NULL, &suspend);
++ if(suspend) {
++ /* we'll come back here later to continue */
++ if(!validate_suspend_setup_timer(qstate, vq,
++ id, VAL_FINDKEY_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
++ vq->sub_ds_msg = NULL;
++ return 1; /* continue processing ds-response results */
++ } else if(!qstate->blacklist && !vq->chain_blacklist &&
+ (msg=val_find_DS(qstate->env, target_key_name,
+ target_key_len, vq->qchase.qclass, qstate->region,
+ vq->key_entry->name)) ) {
+ verbose(VERB_ALGO, "Process cached DS response");
+ process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
+- msg, &msg->qinfo, NULL);
++ msg, &msg->qinfo, NULL, &suspend);
++ if(suspend) {
++ /* we'll come back here later to continue */
++ if(!validate_suspend_setup_timer(qstate, vq,
++ id, VAL_FINDKEY_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ return 1; /* continue processing ds-response results */
+ }
+ if(!generate_request(qstate, id, target_key_name,
+@@ -2014,7 +2125,7 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ struct val_env* ve, int id)
+ {
+ enum val_classification subtype;
+- int rcode, suspend;
++ int rcode, suspend, nsec3_calculations = 0;
+
+ if(!vq->key_entry) {
+ verbose(VERB_ALGO, "validate: no key entry, failed");
+@@ -2072,8 +2183,8 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ if(!validate_msg_signatures(qstate, vq, qstate->env, ve, &vq->qchase,
+ vq->chase_reply, vq->key_entry, &suspend)) {
+ if(suspend) {
+- if(!validate_msg_signatures_setup_timer(qstate, vq,
+- id))
++ if(!validate_suspend_setup_timer(qstate, vq,
++ id, VAL_VALIDATE_STATE))
+ return val_error(qstate, id);
+ return 0;
+ }
+@@ -2105,7 +2216,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ case VAL_CLASS_POSITIVE:
+ verbose(VERB_ALGO, "Validating a positive response");
+ validate_positive_response(qstate->env, ve,
+- &vq->qchase, vq->chase_reply, vq->key_entry);
++ &vq->qchase, vq->chase_reply, vq->key_entry,
++ qstate, vq, &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(positive): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2114,7 +2232,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ case VAL_CLASS_NODATA:
+ verbose(VERB_ALGO, "Validating a nodata response");
+ validate_nodata_response(qstate->env, ve,
+- &vq->qchase, vq->chase_reply, vq->key_entry);
++ &vq->qchase, vq->chase_reply, vq->key_entry,
++ qstate, vq, &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(nodata): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2124,7 +2249,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ rcode = (int)FLAGS_GET_RCODE(vq->orig_msg->rep->flags);
+ verbose(VERB_ALGO, "Validating a nxdomain response");
+ validate_nameerror_response(qstate->env, ve,
+- &vq->qchase, vq->chase_reply, vq->key_entry, &rcode);
++ &vq->qchase, vq->chase_reply, vq->key_entry, &rcode,
++ qstate, vq, &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(nxdomain): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2135,7 +2267,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ case VAL_CLASS_CNAME:
+ verbose(VERB_ALGO, "Validating a cname response");
+ validate_cname_response(qstate->env, ve,
+- &vq->qchase, vq->chase_reply, vq->key_entry);
++ &vq->qchase, vq->chase_reply, vq->key_entry,
++ qstate, vq, &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(cname): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2145,7 +2284,14 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ verbose(VERB_ALGO, "Validating a cname noanswer "
+ "response");
+ validate_cname_noanswer_response(qstate->env, ve,
+- &vq->qchase, vq->chase_reply, vq->key_entry);
++ &vq->qchase, vq->chase_reply, vq->key_entry,
++ qstate, vq, &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(cname_noanswer): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2162,8 +2308,15 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq,
+ case VAL_CLASS_ANY:
+ verbose(VERB_ALGO, "Validating a positive ANY "
+ "response");
+- validate_any_response(qstate->env, ve, &vq->qchase,
+- vq->chase_reply, vq->key_entry);
++ validate_any_response(qstate->env, ve, &vq->qchase,
++ vq->chase_reply, vq->key_entry, qstate, vq,
++ &nsec3_calculations, &suspend);
++ if(suspend) {
++ if(!validate_suspend_setup_timer(qstate,
++ vq, id, VAL_VALIDATE_STATE))
++ return val_error(qstate, id);
++ return 0;
++ }
+ verbose(VERB_DETAIL, "validate(positive_any): %s",
+ sec_status_to_string(
+ vq->chase_reply->security));
+@@ -2586,7 +2739,10 @@ primeResponseToKE(struct ub_packed_rrset_key* dnskey_rrset,
+ * DS response indicated an end to secure space, is_good if the DS
+ * validated. It returns ke=NULL if the DS response indicated that the
+ * request wasn't a delegation point.
+- * @return 0 on servfail error (malloc failure).
++ * @return
++ * 0 on success,
++ * 1 on servfail error (malloc failure),
++ * 2 on NSEC3 suspend.
+ */
+ static int
+ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+@@ -2646,7 +2802,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ *ke = key_entry_create_null(qstate->region,
+ qinfo->qname, qinfo->qname_len, qinfo->qclass,
+ ub_packed_rrset_ttl(ds), *qstate->env->now);
+- return (*ke) != NULL;
++ return (*ke) == NULL;
+ }
+
+ /* Otherwise, we return the positive response. */
+@@ -2654,7 +2810,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ *ke = key_entry_create_rrset(qstate->region,
+ qinfo->qname, qinfo->qname_len, qinfo->qclass, ds,
+ NULL, *qstate->env->now);
+- return (*ke) != NULL;
++ return (*ke) == NULL;
+ } else if(subtype == VAL_CLASS_NODATA ||
+ subtype == VAL_CLASS_NAMEERROR) {
+ /* NODATA means that the qname exists, but that there was
+@@ -2686,12 +2842,12 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ qinfo->qname, qinfo->qname_len,
+ qinfo->qclass, proof_ttl,
+ *qstate->env->now);
+- return (*ke) != NULL;
++ return (*ke) == NULL;
+ case sec_status_insecure:
+ verbose(VERB_DETAIL, "NSEC RRset for the "
+ "referral proved not a delegation point");
+ *ke = NULL;
+- return 1;
++ return 0;
+ case sec_status_bogus:
+ verbose(VERB_DETAIL, "NSEC RRset for the "
+ "referral did not prove no DS.");
+@@ -2703,10 +2859,17 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ break;
+ }
+
++ if(!nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
++ log_err("malloc failure in ds_response_to_ke for "
++ "NSEC3 cache");
++ reason = "malloc failure";
++ errinf_ede(qstate, reason, 0);
++ goto return_bogus;
++ }
+ sec = nsec3_prove_nods(qstate->env, ve,
+ msg->rep->rrsets + msg->rep->an_numrrsets,
+ msg->rep->ns_numrrsets, qinfo, vq->key_entry, &reason,
+- &reason_bogus, qstate);
++ &reason_bogus, qstate, &vq->nsec3_cache_table);
+ switch(sec) {
+ case sec_status_insecure:
+ /* case insecure also continues to unsigned
+@@ -2719,18 +2882,19 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ qinfo->qname, qinfo->qname_len,
+ qinfo->qclass, proof_ttl,
+ *qstate->env->now);
+- return (*ke) != NULL;
++ return (*ke) == NULL;
+ case sec_status_indeterminate:
+ verbose(VERB_DETAIL, "NSEC3s for the "
+ "referral proved no delegation");
+ *ke = NULL;
+- return 1;
++ return 0;
+ case sec_status_bogus:
+ verbose(VERB_DETAIL, "NSEC3s for the "
+ "referral did not prove no DS.");
+ errinf_ede(qstate, reason, reason_bogus);
+ goto return_bogus;
+ case sec_status_unchecked:
++ return 2;
+ default:
+ /* NSEC3 proof did not work */
+ break;
+@@ -2773,7 +2937,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
+ "proof that DS does not exist");
+ /* and that it is not a referral point */
+ *ke = NULL;
+- return 1;
++ return 0;
+ }
+ errinf(qstate, "CNAME in DS response was not secure.");
+ errinf(qstate, reason);
+@@ -2796,7 +2960,7 @@ return_bogus:
+ *ke = key_entry_create_bad(qstate->region, qinfo->qname,
+ qinfo->qname_len, qinfo->qclass,
+ BOGUS_KEY_TTL, *qstate->env->now);
+- return (*ke) != NULL;
++ return (*ke) == NULL;
+ }
+
+ /**
+@@ -2817,17 +2981,31 @@ return_bogus:
+ static void
+ process_ds_response(struct module_qstate* qstate, struct val_qstate* vq,
+ int id, int rcode, struct dns_msg* msg, struct query_info* qinfo,
+- struct sock_list* origin)
++ struct sock_list* origin, int* suspend)
+ {
+ struct val_env* ve = (struct val_env*)qstate->env->modinfo[id];
+ struct key_entry_key* dske = NULL;
+ uint8_t* olds = vq->empty_DS_name;
++ int ret;
++ *suspend = 0;
+ vq->empty_DS_name = NULL;
+- if(!ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske)) {
++ ret = ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske);
++ if(ret != 0) {
++ switch(ret) {
++ case 1:
+ log_err("malloc failure in process_ds_response");
+ vq->key_entry = NULL; /* make it error */
+ vq->state = VAL_VALIDATE_STATE;
+ return;
++ case 2:
++ *suspend = 1;
++ return;
++ default:
++ log_err("unhandled error value for ds_response_to_ke");
++ vq->key_entry = NULL; /* make it error */
++ vq->state = VAL_VALIDATE_STATE;
++ return;
++ }
+ }
+ if(dske == NULL) {
+ vq->empty_DS_name = regional_alloc_init(qstate->region,
+@@ -3074,9 +3252,26 @@ val_inform_super(struct module_qstate* qstate, int id,
+ return;
+ }
+ if(qstate->qinfo.qtype == LDNS_RR_TYPE_DS) {
++ int suspend;
+ process_ds_response(super, vq, id, qstate->return_rcode,
+- qstate->return_msg, &qstate->qinfo,
+- qstate->reply_origin);
++ qstate->return_msg, &qstate->qinfo,
++ qstate->reply_origin, &suspend);
++ /* If NSEC3 was needed during validation, NULL the NSEC3 cache;
++ * it will be re-initiated if needed later on.
++ * Validation (and the cache table) are happening/allocated in
++ * the super qstate whilst the RRs are allocated (and pointed
++ * to) in this sub qstate. */
++ if(vq->nsec3_cache_table.ct) {
++ vq->nsec3_cache_table.ct = NULL;
++ }
++ if(suspend) {
++ /* deep copy the return_msg to vq->sub_ds_msg; it will
++ * be resumed later in the super state with the caveat
++ * that the initial calculations will be re-caclulated
++ * and re-suspended there before continuing. */
++ vq->sub_ds_msg = dns_msg_deepcopy_region(
++ qstate->return_msg, super->region);
++ }
+ return;
+ } else if(qstate->qinfo.qtype == LDNS_RR_TYPE_DNSKEY) {
+ process_dnskey_response(super, vq, id, qstate->return_rcode,
+@@ -3095,8 +3290,8 @@ val_clear(struct module_qstate* qstate, int id)
+ return;
+ vq = (struct val_qstate*)qstate->minfo[id];
+ if(vq) {
+- if(vq->msg_signatures_timer) {
+- comm_timer_delete(vq->msg_signatures_timer);
++ if(vq->suspend_timer) {
++ comm_timer_delete(vq->suspend_timer);
+ }
+ }
+ /* everything is allocated in the region, so assign NULL */
+diff --git a/validator/validator.h b/validator/validator.h
+index a997ca88f..72f44b16e 100644
+--- a/validator/validator.h
++++ b/validator/validator.h
+@@ -45,6 +45,7 @@
+ #include "util/module.h"
+ #include "util/data/msgreply.h"
+ #include "validator/val_utils.h"
++#include "validator/val_nsec3.h"
+ struct val_anchors;
+ struct key_cache;
+ struct key_entry_key;
+@@ -221,10 +222,14 @@ struct val_qstate {
+ int msg_signatures_state;
+ /** The rrset index for the msg signatures to continue from */
+ size_t msg_signatures_index;
++ /** Cache table for NSEC3 hashes */
++ struct nsec3_cache_table nsec3_cache_table;
++ /** DS message from sub if it got suspended from NSEC3 calculations */
++ struct dns_msg* sub_ds_msg;
+ /** The timer to resume processing msg signatures */
+- struct comm_timer* msg_signatures_timer;
+- /** number of suspends */
+- int suspend_count;
++ struct comm_timer* suspend_timer;
++ /** Number of suspends */
++ int suspend_count;
+ };
+
+ /**
+@@ -273,6 +278,6 @@ void val_clear(struct module_qstate* qstate, int id);
+ size_t val_get_mem(struct module_env* env, int id);
+
+ /** Timer callback for msg signatures continue timer */
+-void validate_msg_signatures_timer_cb(void* arg);
++void validate_suspend_timer_cb(void* arg);
+
+ #endif /* VALIDATOR_VALIDATOR_H */
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/CVE-2024-33655.patch unbound-1.17.1/debian/patches/CVE-2024-33655.patch
--- unbound-1.17.1/debian/patches/CVE-2024-33655.patch 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2024-33655.patch 2025-11-30 10:12:22.000000000 +0300
@@ -18,7 +18,6 @@
.../doh_downstream_notls.conf | 1 +
.../doh_downstream_post.conf | 1 +
.../fwd_three_service.tdir/fwd_three_service.conf | 1 +
- testdata/fwd_udptmout.tdir/fwd_udptmout.conf | 1 +
testdata/iter_ghost_timewindow.rpl | 1 +
testdata/ssl_req_order.tdir/ssl_req_order.conf | 1 +
testdata/tcp_req_order.tdir/tcp_req_order.conf | 1 +
@@ -27,7 +26,7 @@
util/config_file.h | 9 ++
util/configlexer.lex | 3 +
util/configparser.y | 33 +++++
- 18 files changed, 335 insertions(+), 3 deletions(-)
+ 17 files changed, 334 insertions(+), 3 deletions(-)
diff --git a/doc/example.conf.in b/doc/example.conf.in
index 8cf3d86..237cc05 100644
@@ -525,18 +524,6 @@
forward-zone:
name: "."
forward-addr: "127.0.0.1@@TOPORT@"
-diff --git a/testdata/fwd_udptmout.tdir/fwd_udptmout.conf b/testdata/fwd_udptmout.tdir/fwd_udptmout.conf
-index d5135a1..0d37de9 100644
---- a/testdata/fwd_udptmout.tdir/fwd_udptmout.conf
-+++ b/testdata/fwd_udptmout.tdir/fwd_udptmout.conf
-@@ -11,6 +11,7 @@ server:
- num-queries-per-thread: 1024
- use-syslog: no
- do-not-query-localhost: no
-+ discard-timeout: 18000 # testns uses sleep=15+2
- forward-zone:
- name: "."
- forward-addr: "127.0.0.1@@TOPORT@"
diff --git a/testdata/iter_ghost_timewindow.rpl b/testdata/iter_ghost_timewindow.rpl
index 566be82..9e30462 100644
--- a/testdata/iter_ghost_timewindow.rpl
diff -Nru unbound-1.17.1/debian/patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch unbound-1.17.1/debian/patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch
--- unbound-1.17.1/debian/patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,44 @@
+From: Michael Tokarev <mjt@tls.msk.ru>
+Date: Sun, 30 Nov 2025 10:59:38 +0300
+Subject: iterator/iter_scrub.c: pass module_env parameter to scrub_normalize()
+
+This is a part of upstream commit 8df1e58209458b9ff62b00c29d01964570d82cbb
+"Add harden-unknown-additional option":
+https://github.com/NLnetLabs/unbound/commit/8df1e58209458b9ff62b00c29d01964570d82cbb
+The only 2 minimal changes are needed for the subsequent fix in this area, -
+passing extra `env' argumet to scrub_normalize().
+---
+ iterator/iter_scrub.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c
+index f093c1bf9..04344ae26 100644
+--- a/iterator/iter_scrub.c
++++ b/iterator/iter_scrub.c
+@@ -355,11 +355,13 @@ soa_in_auth(struct msg_parse* msg)
+ * @param msg: msg to normalize.
+ * @param qinfo: original query.
+ * @param region: where to allocate synthesized CNAMEs.
++ * @param env: module env with config options.
+ * @return 0 on error.
+ */
+ static int
+ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
+- struct query_info* qinfo, struct regional* region)
++ struct query_info* qinfo, struct regional* region,
++ struct module_env* env)
+ {
+ uint8_t* sname = qinfo->qname;
+ size_t snamelen = qinfo->qname_len;
+@@ -846,7 +848,7 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
+ }
+
+ /* normalize the response, this cleans up the additional. */
+- if(!scrub_normalize(pkt, msg, qinfo, region))
++ if(!scrub_normalize(pkt, msg, qinfo, region, env))
+ return 0;
+ /* delete all out-of-zone information */
+ if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie))
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch unbound-1.17.1/debian/patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch
--- unbound-1.17.1/debian/patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,2167 @@
+From: Yorgos Thessalonikefs <yorgos@nlnetlabs.nl>
+Date: Wed, 22 Oct 2025 10:54:57 +0200
+Subject: CVE-2025-11411 (possible domain hijacking attack)
+
+reported by Yuxiao Wu, Yunyi Zhang, Baojun Liu and Haixin Duan
+from Tsinghua University.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/a33f0638e1dacf2633cf2292078a674576bca852
+Bug: https://nlnetlabs.nl/downloads/unbound/CVE-2025-11411.txt
+Bug-Debian-Security: //security-tracker.debian.org/tracker/CVE-2025-11411
+Comment: back-ported to 1.17 (with other changes) by Michael Tokarev <mjt@tls.msk.ru>
+---
+ doc/example.conf.in | 4 +
+ doc/unbound.conf.5.in | 6 +
+ iterator/iter_scrub.c | 16 +
+ testdata/autotrust_init.rpl | 1 +
+ testdata/autotrust_init_ds.rpl | 1 +
+ testdata/autotrust_init_sigs.rpl | 1 +
+ testdata/autotrust_init_zsk.rpl | 1 +
+ testdata/black_data.rpl | 1 +
+ testdata/black_prime.rpl | 1 +
+ testdata/dns64_lookup.rpl | 1 +
+ testdata/fetch_glue.rpl | 1 +
+ testdata/fetch_glue_cname.rpl | 1 +
+ testdata/fwd_cached.rpl | 1 +
+ .../fwd_compress_c00c.conf | 1 +
+ testdata/fwd_minimal.rpl | 1 +
+ testdata/ipsecmod_bogus_ipseckey.crpl | 1 +
+ testdata/ipsecmod_enabled.crpl | 1 +
+ testdata/ipsecmod_ignore_bogus_ipseckey.crpl | 1 +
+ testdata/ipsecmod_max_ttl.crpl | 1 +
+ testdata/ipsecmod_strict.crpl | 1 +
+ testdata/ipsecmod_whitelist.crpl | 1 +
+ testdata/iter_class_any.rpl | 1 +
+ testdata/iter_cycle_noh.rpl | 1 +
+ testdata/iter_domain_sale.rpl | 1 +
+ testdata/iter_domain_sale_nschange.rpl | 1 +
+ testdata/iter_emptydp.rpl | 1 +
+ testdata/iter_emptydp_for_glue.rpl | 1 +
+ testdata/iter_fwdfirst.rpl | 1 +
+ testdata/iter_fwdfirstequal.rpl | 1 +
+ testdata/iter_fwdstub.rpl | 1 +
+ testdata/iter_fwdstubroot.rpl | 1 +
+ testdata/iter_ghost_sub.rpl | 1 +
+ testdata/iter_ghost_timewindow.rpl | 1 +
+ testdata/iter_got6only.rpl | 1 +
+ testdata/iter_hint_lame.rpl | 1 +
+ testdata/iter_lame_noaa.rpl | 1 +
+ testdata/iter_lame_nosoa.rpl | 1 +
+ testdata/iter_mod.rpl | 1 +
+ testdata/iter_ns_badip.rpl | 1 +
+ testdata/iter_ns_spoof.rpl | 1 +
+ testdata/iter_nxns_fallback.rpl | 1 +
+ testdata/iter_pc_a.rpl | 1 +
+ testdata/iter_pc_aaaa.rpl | 1 +
+ testdata/iter_pcdiff.rpl | 1 +
+ testdata/iter_pcdirect.rpl | 1 +
+ testdata/iter_pcname.rpl | 1 +
+ testdata/iter_pcnamech.rpl | 1 +
+ testdata/iter_pcnamechrec.rpl | 1 +
+ testdata/iter_pcnamerec.rpl | 1 +
+ testdata/iter_pcttl.rpl | 1 +
+ testdata/iter_prefetch.rpl | 1 +
+ testdata/iter_prefetch_change.rpl | 1 +
+ testdata/iter_prefetch_change2.rpl | 1 +
+ testdata/iter_prefetch_childns.rpl | 1 +
+ testdata/iter_prefetch_fail.rpl | 1 +
+ testdata/iter_prefetch_ns.rpl | 1 +
+ testdata/iter_primenoglue.rpl | 1 +
+ testdata/iter_privaddr.rpl | 1 +
+ testdata/iter_ranoaa_lame.rpl | 1 +
+ testdata/iter_reclame_one.rpl | 1 +
+ testdata/iter_reclame_two.rpl | 1 +
+ testdata/iter_recurse.rpl | 1 +
+ testdata/iter_resolve.rpl | 1 +
+ testdata/iter_resolve_minimised.rpl | 1 +
+ testdata/iter_resolve_minimised_nx.rpl | 1 +
+ testdata/iter_resolve_minimised_refused.rpl | 1 +
+ testdata/iter_resolve_minimised_timeout.rpl | 1 +
+ testdata/iter_scrub_cname_an.rpl | 1 +
+ testdata/iter_scrub_dname_insec.rpl | 1 +
+ testdata/iter_scrub_dname_rev.rpl | 1 +
+ testdata/iter_scrub_dname_sec.rpl | 1 +
+ testdata/iter_scrub_promiscuous.rpl | 373 ++++++++++++++++++
+ testdata/iter_soamin.rpl | 1 +
+ testdata/iter_stub_noroot.rpl | 1 +
+ testdata/iter_stubfirst.rpl | 1 +
+ testdata/iter_timeout_ra_aaaa.rpl | 1 +
+ testdata/rrset_rettl.rpl | 1 +
+ testdata/rrset_untrusted.rpl | 1 +
+ testdata/rrset_updated.rpl | 1 +
+ testdata/serve_expired.rpl | 1 +
+ testdata/serve_expired_cached_servfail.rpl | 1 +
+ testdata/serve_expired_client_timeout.rpl | 1 +
+ ...rve_expired_client_timeout_no_prefetch.rpl | 1 +
+ .../serve_expired_client_timeout_servfail.rpl | 1 +
+ testdata/serve_expired_reply_ttl.rpl | 1 +
+ testdata/serve_expired_ttl.rpl | 1 +
+ testdata/serve_expired_ttl_client_timeout.rpl | 1 +
+ testdata/serve_expired_zerottl.rpl | 1 +
+ testdata/serve_original_ttl.rpl | 1 +
+ testdata/subnet_cached.crpl | 1 +
+ testdata/subnet_cached_servfail.crpl | 1 +
+ testdata/subnet_max_source.crpl | 1 +
+ testdata/subnet_prefetch.crpl | 1 +
+ testdata/subnet_prefetch_with_client_ecs.crpl | 1 +
+ testdata/subnet_val_positive.crpl | 1 +
+ testdata/subnet_val_positive_client.crpl | 1 +
+ testdata/trust_cname_chain.rpl | 1 +
+ testdata/ttl_max.rpl | 1 +
+ testdata/ttl_min.rpl | 1 +
+ testdata/val_adbit.rpl | 1 +
+ testdata/val_adcopy.rpl | 1 +
+ testdata/val_cnametocnamewctoposwc.rpl | 1 +
+ testdata/val_ds_afterprime.rpl | 1 +
+ testdata/val_faildnskey_ok.rpl | 1 +
+ testdata/val_keyprefetch_verify.rpl | 1 +
+ testdata/val_noadwhennodo.rpl | 1 +
+ testdata/val_nsec3_b3_optout.rpl | 1 +
+ testdata/val_nsec3_b3_optout_negcache.rpl | 1 +
+ testdata/val_nsec3_b4_wild.rpl | 1 +
+ testdata/val_nsec3_cnametocnamewctoposwc.rpl | 1 +
+ testdata/val_positive.rpl | 1 +
+ testdata/val_positive_wc.rpl | 1 +
+ testdata/val_qds_badanc.rpl | 1 +
+ testdata/val_qds_oneanc.rpl | 1 +
+ testdata/val_qds_twoanc.rpl | 1 +
+ testdata/val_refer_unsignadd.rpl | 1 +
+ testdata/val_referd.rpl | 1 +
+ testdata/val_referglue.rpl | 1 +
+ testdata/val_rrsig.rpl | 1 +
+ testdata/val_spurious_ns.rpl | 1 +
+ testdata/val_stub_noroot.rpl | 1 +
+ testdata/val_ta_algo_dnskey.rpl | 1 +
+ testdata/val_ta_algo_dnskey_dp.rpl | 1 +
+ testdata/val_ta_algo_missing_dp.rpl | 1 +
+ testdata/val_twocname.rpl | 1 +
+ testdata/val_unalgo_anchor.rpl | 1 +
+ testdata/val_wild_pos.rpl | 1 +
+ testdata/views.rpl | 1 +
+ util/config_file.c | 3 +
+ util/config_file.h | 3 +
+ util/configlexer.lex | 1 +
+ util/configparser.y | 12 +
+ 132 files changed, 542 insertions(+)
+ create mode 100644 testdata/iter_scrub_promiscuous.rpl
+
+diff --git a/doc/example.conf.in b/doc/example.conf.in
+index 237cc05cb..5c46843a2 100644
+--- a/doc/example.conf.in
++++ b/doc/example.conf.in
+@@ -187,6 +187,10 @@ server:
+ # query upon encountering a CNAME record.
+ # max-query-restarts: 11
+
++ # Should the scrubber remove promiscuous NS from positive answers,
++ # protects against poison attempts.
++ # iter-scrub-promiscuous: yes
++
+ # msec for waiting for an unknown server to reply. Increase if you
+ # are behind a slow satellite link, to eg. 1128.
+ # unknown-server-time-limit: 376
+diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in
+index 4acc26f05..b4a4b6c0c 100644
+--- a/doc/unbound.conf.5.in
++++ b/doc/unbound.conf.5.in
+@@ -1863,6 +1863,12 @@ Changing this value needs caution as it can allow long CNAME chains to be
+ accepted, where Unbound needs to verify (resolve) each link individually.
+ Default is 11.
+ .TP 5
++.B iter\-scrub\-promiscuous: \fI<yes or no>
++Should the iterator scrubber remove promiscuous NS from positive answers.
++This protects against poisonous contents, that could affect names in the
++same zone as a spoofed packet.
++Default is yes.
++.TP 5
+ .B fast\-server\-permil: \fI<number>
+ Specify how many times out of 1000 to pick from the set of fastest servers.
+ 0 turns the feature off. A value of 900 would pick from the fastest
+diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c
+index 04344ae26..b7f91e145 100644
+--- a/iterator/iter_scrub.c
++++ b/iterator/iter_scrub.c
+@@ -542,6 +542,22 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
+ "RRset:", pkt, msg, prev, &rrset);
+ continue;
+ }
++ /* If the NS set is a promiscuous NS set, scrub that
++ * to remove potential for poisonous contents that
++ * affects other names in the same zone. Remove
++ * promiscuous NS sets in positive answers, that
++ * thus have records in the answer section. Nodata
++ * and nxdomain promiscuous NS sets have been removed
++ * already. Since the NS rrset is scrubbed, its
++ * address records are also not marked to be allowed
++ * and are removed later. */
++ if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR &&
++ msg->an_rrsets != 0 &&
++ env->cfg->iter_scrub_promiscuous) {
++ remove_rrset("normalize: removing promiscuous "
++ "RRset:", pkt, msg, prev, &rrset);
++ continue;
++ }
+ if(nsset == NULL) {
+ nsset = rrset;
+ } else {
+diff --git a/testdata/autotrust_init.rpl b/testdata/autotrust_init.rpl
+index d722273e0..d69e70b4b 100644
+--- a/testdata/autotrust_init.rpl
++++ b/testdata/autotrust_init.rpl
+@@ -5,6 +5,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/autotrust_init_ds.rpl b/testdata/autotrust_init_ds.rpl
+index ad4019ebe..9ffb4d4ba 100644
+--- a/testdata/autotrust_init_ds.rpl
++++ b/testdata/autotrust_init_ds.rpl
+@@ -5,6 +5,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/autotrust_init_sigs.rpl b/testdata/autotrust_init_sigs.rpl
+index d5d52f473..a7cb7963b 100644
+--- a/testdata/autotrust_init_sigs.rpl
++++ b/testdata/autotrust_init_sigs.rpl
+@@ -5,6 +5,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/autotrust_init_zsk.rpl b/testdata/autotrust_init_zsk.rpl
+index 56a5bc0b3..2d28d4340 100644
+--- a/testdata/autotrust_init_zsk.rpl
++++ b/testdata/autotrust_init_zsk.rpl
+@@ -5,6 +5,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/black_data.rpl b/testdata/black_data.rpl
+index e6ef1b79d..e928d630d 100644
+--- a/testdata/black_data.rpl
++++ b/testdata/black_data.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/black_prime.rpl b/testdata/black_prime.rpl
+index fbe92a721..0301c85b6 100644
+--- a/testdata/black_prime.rpl
++++ b/testdata/black_prime.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/dns64_lookup.rpl b/testdata/dns64_lookup.rpl
+index 898d0d01a..0881c4d25 100644
+--- a/testdata/dns64_lookup.rpl
++++ b/testdata/dns64_lookup.rpl
+@@ -5,6 +5,7 @@ server:
+ module-config: "dns64 validator iterator"
+ dns64-prefix: 64:ff9b::0/96
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/fetch_glue.rpl b/testdata/fetch_glue.rpl
+index 8860d85b0..daf687ad4 100644
+--- a/testdata/fetch_glue.rpl
++++ b/testdata/fetch_glue.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/fetch_glue_cname.rpl b/testdata/fetch_glue_cname.rpl
+index 64f00fb20..c786a417c 100644
+--- a/testdata/fetch_glue_cname.rpl
++++ b/testdata/fetch_glue_cname.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/fwd_cached.rpl b/testdata/fwd_cached.rpl
+index 2d6b0c2b8..4a00f8715 100644
+--- a/testdata/fwd_cached.rpl
++++ b/testdata/fwd_cached.rpl
+@@ -2,6 +2,7 @@
+ ; config options go here.
+ server:
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ forward-zone: name: "." forward-addr: 216.0.0.1
+ CONFIG_END
+
+diff --git a/testdata/fwd_compress_c00c.tdir/fwd_compress_c00c.conf b/testdata/fwd_compress_c00c.tdir/fwd_compress_c00c.conf
+index 5b2c8045a..7bc7408cd 100644
+--- a/testdata/fwd_compress_c00c.tdir/fwd_compress_c00c.conf
++++ b/testdata/fwd_compress_c00c.tdir/fwd_compress_c00c.conf
+@@ -10,6 +10,7 @@ server:
+ username: ""
+ do-not-query-localhost: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+ forward-zone:
+ name: "."
+diff --git a/testdata/fwd_minimal.rpl b/testdata/fwd_minimal.rpl
+index e85d7124b..ef1d7fc41 100644
+--- a/testdata/fwd_minimal.rpl
++++ b/testdata/fwd_minimal.rpl
+@@ -5,6 +5,7 @@ server:
+ ; is fine for that, not removed by minimal-responses.
+ access-control: 127.0.0.1 allow_snoop
+ minimal-responses: yes
++ iter-scrub-promiscuous: no
+ forward-zone: name: "." forward-addr: 216.0.0.1
+ CONFIG_END
+
+diff --git a/testdata/ipsecmod_bogus_ipseckey.crpl b/testdata/ipsecmod_bogus_ipseckey.crpl
+index 094710b60..98bc454f2 100644
+--- a/testdata/ipsecmod_bogus_ipseckey.crpl
++++ b/testdata/ipsecmod_bogus_ipseckey.crpl
+@@ -9,6 +9,7 @@ server:
+ qname-minimisation: "no"
+ # test that default value of harden-dnssec-stripped is still yes.
+ fake-sha1: yes
++ iter-scrub-promiscuous: no
+ trust-anchor-signaling: no
+ access-control: 127.0.0.1 allow_snoop
+ module-config: "ipsecmod validator iterator"
+diff --git a/testdata/ipsecmod_enabled.crpl b/testdata/ipsecmod_enabled.crpl
+index 449842961..04e8cb1a1 100644
+--- a/testdata/ipsecmod_enabled.crpl
++++ b/testdata/ipsecmod_enabled.crpl
+@@ -11,6 +11,7 @@ server:
+ ipsecmod-enabled: no
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/ipsecmod_ignore_bogus_ipseckey.crpl b/testdata/ipsecmod_ignore_bogus_ipseckey.crpl
+index a605c3445..4c4d80c10 100644
+--- a/testdata/ipsecmod_ignore_bogus_ipseckey.crpl
++++ b/testdata/ipsecmod_ignore_bogus_ipseckey.crpl
+@@ -18,6 +18,7 @@ server:
+ ipsecmod-ignore-bogus: yes
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/ipsecmod_max_ttl.crpl b/testdata/ipsecmod_max_ttl.crpl
+index 592bae046..4dfeddfd9 100644
+--- a/testdata/ipsecmod_max_ttl.crpl
++++ b/testdata/ipsecmod_max_ttl.crpl
+@@ -10,6 +10,7 @@ server:
+ ipsecmod-max-ttl: 200
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/ipsecmod_strict.crpl b/testdata/ipsecmod_strict.crpl
+index f74e308bd..51cc11b53 100644
+--- a/testdata/ipsecmod_strict.crpl
++++ b/testdata/ipsecmod_strict.crpl
+@@ -10,6 +10,7 @@ server:
+ ipsecmod-max-ttl: 200
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/ipsecmod_whitelist.crpl b/testdata/ipsecmod_whitelist.crpl
+index 34108f3b1..350c2ad48 100644
+--- a/testdata/ipsecmod_whitelist.crpl
++++ b/testdata/ipsecmod_whitelist.crpl
+@@ -11,6 +11,7 @@ server:
+ ipsecmod-whitelist: white.example.com
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_class_any.rpl b/testdata/iter_class_any.rpl
+index 6fb296e99..87e0db032 100644
+--- a/testdata/iter_class_any.rpl
++++ b/testdata/iter_class_any.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_cycle_noh.rpl b/testdata/iter_cycle_noh.rpl
+index eee26ca70..e551ac6e8 100644
+--- a/testdata/iter_cycle_noh.rpl
++++ b/testdata/iter_cycle_noh.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_domain_sale.rpl b/testdata/iter_domain_sale.rpl
+index 6110148a3..7c3cc1f2f 100644
+--- a/testdata/iter_domain_sale.rpl
++++ b/testdata/iter_domain_sale.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_domain_sale_nschange.rpl b/testdata/iter_domain_sale_nschange.rpl
+index 5664855d5..886ed51a3 100644
+--- a/testdata/iter_domain_sale_nschange.rpl
++++ b/testdata/iter_domain_sale_nschange.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_emptydp.rpl b/testdata/iter_emptydp.rpl
+index 82ddccfad..17b60596c 100644
+--- a/testdata/iter_emptydp.rpl
++++ b/testdata/iter_emptydp.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_emptydp_for_glue.rpl b/testdata/iter_emptydp_for_glue.rpl
+index 68fad6f15..e86ad147c 100644
+--- a/testdata/iter_emptydp_for_glue.rpl
++++ b/testdata/iter_emptydp_for_glue.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_fwdfirst.rpl b/testdata/iter_fwdfirst.rpl
+index 0f8a85f5a..509a1cdad 100644
+--- a/testdata/iter_fwdfirst.rpl
++++ b/testdata/iter_fwdfirst.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_fwdfirstequal.rpl b/testdata/iter_fwdfirstequal.rpl
+index dc648143c..abd25d149 100644
+--- a/testdata/iter_fwdfirstequal.rpl
++++ b/testdata/iter_fwdfirstequal.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_fwdstub.rpl b/testdata/iter_fwdstub.rpl
+index ad5b57cb7..4c741a50f 100644
+--- a/testdata/iter_fwdstub.rpl
++++ b/testdata/iter_fwdstub.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_fwdstubroot.rpl b/testdata/iter_fwdstubroot.rpl
+index fa930430d..dd93ecdef 100644
+--- a/testdata/iter_fwdstubroot.rpl
++++ b/testdata/iter_fwdstubroot.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_ghost_sub.rpl b/testdata/iter_ghost_sub.rpl
+index ccd6b2984..4eced05ea 100644
+--- a/testdata/iter_ghost_sub.rpl
++++ b/testdata/iter_ghost_sub.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_ghost_timewindow.rpl b/testdata/iter_ghost_timewindow.rpl
+index 9e304628c..24390a09c 100644
+--- a/testdata/iter_ghost_timewindow.rpl
++++ b/testdata/iter_ghost_timewindow.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ discard-timeout: 86400
+
+ stub-zone:
+diff --git a/testdata/iter_got6only.rpl b/testdata/iter_got6only.rpl
+index 155228439..b0d20b3f4 100644
+--- a/testdata/iter_got6only.rpl
++++ b/testdata/iter_got6only.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0 "
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/iter_hint_lame.rpl b/testdata/iter_hint_lame.rpl
+index 2fb6dde72..26aa5dc73 100644
+--- a/testdata/iter_hint_lame.rpl
++++ b/testdata/iter_hint_lame.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_lame_noaa.rpl b/testdata/iter_lame_noaa.rpl
+index defaa5ca8..050866c65 100644
+--- a/testdata/iter_lame_noaa.rpl
++++ b/testdata/iter_lame_noaa.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_lame_nosoa.rpl b/testdata/iter_lame_nosoa.rpl
+index 3bf6ccc18..d55ff78d6 100644
+--- a/testdata/iter_lame_nosoa.rpl
++++ b/testdata/iter_lame_nosoa.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_mod.rpl b/testdata/iter_mod.rpl
+index 35b3a5af6..3d3d6789d 100644
+--- a/testdata/iter_mod.rpl
++++ b/testdata/iter_mod.rpl
+@@ -4,6 +4,7 @@ server:
+ qname-minimisation: "no"
+ module-config: "iterator"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_ns_badip.rpl b/testdata/iter_ns_badip.rpl
+index e0bf96674..481f47a0a 100644
+--- a/testdata/iter_ns_badip.rpl
++++ b/testdata/iter_ns_badip.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "3 2 1 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_ns_spoof.rpl b/testdata/iter_ns_spoof.rpl
+index f67457635..999ff05ff 100644
+--- a/testdata/iter_ns_spoof.rpl
++++ b/testdata/iter_ns_spoof.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/iter_nxns_fallback.rpl b/testdata/iter_nxns_fallback.rpl
+index 324068604..7243c6b0f 100644
+--- a/testdata/iter_nxns_fallback.rpl
++++ b/testdata/iter_nxns_fallback.rpl
+@@ -8,6 +8,7 @@ server:
+ access-control: 127.0.0.1 allow_snoop
+ qname-minimisation: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_pc_a.rpl b/testdata/iter_pc_a.rpl
+index d9add0056..be73a796a 100644
+--- a/testdata/iter_pc_a.rpl
++++ b/testdata/iter_pc_a.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pc_aaaa.rpl b/testdata/iter_pc_aaaa.rpl
+index a28354306..a7ce1866f 100644
+--- a/testdata/iter_pc_aaaa.rpl
++++ b/testdata/iter_pc_aaaa.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pcdiff.rpl b/testdata/iter_pcdiff.rpl
+index 57fb109af..a462d333e 100644
+--- a/testdata/iter_pcdiff.rpl
++++ b/testdata/iter_pcdiff.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pcdirect.rpl b/testdata/iter_pcdirect.rpl
+index 0bd5dfe78..656ec7af4 100644
+--- a/testdata/iter_pcdirect.rpl
++++ b/testdata/iter_pcdirect.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pcname.rpl b/testdata/iter_pcname.rpl
+index e17c9102c..af53c901b 100644
+--- a/testdata/iter_pcname.rpl
++++ b/testdata/iter_pcname.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pcnamech.rpl b/testdata/iter_pcnamech.rpl
+index 32b3130c8..805cb18f7 100644
+--- a/testdata/iter_pcnamech.rpl
++++ b/testdata/iter_pcnamech.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_pcnamechrec.rpl b/testdata/iter_pcnamechrec.rpl
+index 8bf7ad879..bbb9c863d 100644
+--- a/testdata/iter_pcnamechrec.rpl
++++ b/testdata/iter_pcnamechrec.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_pcnamerec.rpl b/testdata/iter_pcnamerec.rpl
+index faee6d029..2ea0dada3 100644
+--- a/testdata/iter_pcnamerec.rpl
++++ b/testdata/iter_pcnamerec.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_pcttl.rpl b/testdata/iter_pcttl.rpl
+index 413f8cb88..a70201710 100644
+--- a/testdata/iter_pcttl.rpl
++++ b/testdata/iter_pcttl.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ do-ip6: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch.rpl b/testdata/iter_prefetch.rpl
+index bad92dc57..fdf595564 100644
+--- a/testdata/iter_prefetch.rpl
++++ b/testdata/iter_prefetch.rpl
+@@ -4,6 +4,7 @@ server:
+ qname-minimisation: "no"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch_change.rpl b/testdata/iter_prefetch_change.rpl
+index 1be9e6abe..c1a1a710f 100644
+--- a/testdata/iter_prefetch_change.rpl
++++ b/testdata/iter_prefetch_change.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch_change2.rpl b/testdata/iter_prefetch_change2.rpl
+index 7a8370ff6..4a966fea0 100644
+--- a/testdata/iter_prefetch_change2.rpl
++++ b/testdata/iter_prefetch_change2.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch_childns.rpl b/testdata/iter_prefetch_childns.rpl
+index 00a91fcde..f234065e7 100644
+--- a/testdata/iter_prefetch_childns.rpl
++++ b/testdata/iter_prefetch_childns.rpl
+@@ -4,6 +4,7 @@ server:
+ qname-minimisation: "no"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch_fail.rpl b/testdata/iter_prefetch_fail.rpl
+index 1d92a4c1c..d1e308305 100644
+--- a/testdata/iter_prefetch_fail.rpl
++++ b/testdata/iter_prefetch_fail.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_prefetch_ns.rpl b/testdata/iter_prefetch_ns.rpl
+index 93af21638..3192d31c0 100644
+--- a/testdata/iter_prefetch_ns.rpl
++++ b/testdata/iter_prefetch_ns.rpl
+@@ -4,6 +4,7 @@ server:
+ qname-minimisation: "no"
+ prefetch: "yes"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_primenoglue.rpl b/testdata/iter_primenoglue.rpl
+index a0be71c78..0b6935ccb 100644
+--- a/testdata/iter_primenoglue.rpl
++++ b/testdata/iter_primenoglue.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_privaddr.rpl b/testdata/iter_privaddr.rpl
+index 93a2a147d..edfced3b8 100644
+--- a/testdata/iter_privaddr.rpl
++++ b/testdata/iter_privaddr.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ private-address: 10.0.0.0/8
+ private-address: 172.16.0.0/12
+diff --git a/testdata/iter_ranoaa_lame.rpl b/testdata/iter_ranoaa_lame.rpl
+index 0e6d98778..13b426a55 100644
+--- a/testdata/iter_ranoaa_lame.rpl
++++ b/testdata/iter_ranoaa_lame.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_reclame_one.rpl b/testdata/iter_reclame_one.rpl
+index 4a6abfae5..d273e6056 100644
+--- a/testdata/iter_reclame_one.rpl
++++ b/testdata/iter_reclame_one.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_reclame_two.rpl b/testdata/iter_reclame_two.rpl
+index 459dcb17f..9919e1132 100644
+--- a/testdata/iter_reclame_two.rpl
++++ b/testdata/iter_reclame_two.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/iter_recurse.rpl b/testdata/iter_recurse.rpl
+index be50b4af8..135287678 100644
+--- a/testdata/iter_recurse.rpl
++++ b/testdata/iter_recurse.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_resolve.rpl b/testdata/iter_resolve.rpl
+index ed051ff24..3ea56abe9 100644
+--- a/testdata/iter_resolve.rpl
++++ b/testdata/iter_resolve.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_resolve_minimised.rpl b/testdata/iter_resolve_minimised.rpl
+index 2c6f9ccf5..13f04d481 100644
+--- a/testdata/iter_resolve_minimised.rpl
++++ b/testdata/iter_resolve_minimised.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_resolve_minimised_nx.rpl b/testdata/iter_resolve_minimised_nx.rpl
+index 74e612ccb..c68f20ca8 100644
+--- a/testdata/iter_resolve_minimised_nx.rpl
++++ b/testdata/iter_resolve_minimised_nx.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_resolve_minimised_refused.rpl b/testdata/iter_resolve_minimised_refused.rpl
+index 66e8e631e..8dc76e258 100644
+--- a/testdata/iter_resolve_minimised_refused.rpl
++++ b/testdata/iter_resolve_minimised_refused.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_resolve_minimised_timeout.rpl b/testdata/iter_resolve_minimised_timeout.rpl
+index 86b932160..3740d79f4 100644
+--- a/testdata/iter_resolve_minimised_timeout.rpl
++++ b/testdata/iter_resolve_minimised_timeout.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_scrub_cname_an.rpl b/testdata/iter_scrub_cname_an.rpl
+index 9c5060af7..f81916b0c 100644
+--- a/testdata/iter_scrub_cname_an.rpl
++++ b/testdata/iter_scrub_cname_an.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_scrub_dname_insec.rpl b/testdata/iter_scrub_dname_insec.rpl
+index 921abe628..e476b2763 100644
+--- a/testdata/iter_scrub_dname_insec.rpl
++++ b/testdata/iter_scrub_dname_insec.rpl
+@@ -4,6 +4,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_scrub_dname_rev.rpl b/testdata/iter_scrub_dname_rev.rpl
+index 9caca66c0..dfb21b8b6 100644
+--- a/testdata/iter_scrub_dname_rev.rpl
++++ b/testdata/iter_scrub_dname_rev.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_scrub_dname_sec.rpl b/testdata/iter_scrub_dname_sec.rpl
+index 34a7b324d..943b19ff5 100644
+--- a/testdata/iter_scrub_dname_sec.rpl
++++ b/testdata/iter_scrub_dname_sec.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_scrub_promiscuous.rpl b/testdata/iter_scrub_promiscuous.rpl
+new file mode 100644
+index 000000000..61fca0d28
+--- /dev/null
++++ b/testdata/iter_scrub_promiscuous.rpl
+@@ -0,0 +1,373 @@
++; config options
++server:
++ target-fetch-policy: "0 0 0 0 0"
++ qname-minimisation: no
++ iter-scrub-promiscuous: yes
++
++stub-zone:
++ name: "."
++ stub-addr: 1.2.3.0 # ns.root
++CONFIG_END
++
++SCENARIO_BEGIN Test iterator with scrub of promiscuous records
++; The test queries receive spoofed answers. The check queries see if
++; the record is returned by the original server or by a spoofed source.
++; The test domains are pollute1.mesa, pollute2.mesa and pollute3.mesa.
++; The spoofed contents are ns.attacker.mesa and its IPs 5.6.7.8 and 5.6.7.9.
++; The pollute1.mesa NS, ns.pollute2.mesa A, and test3.atkr.pollute3.mesa NS
++; with ns.pollute3.mesa A records are tested for cache placement.
++
++; ns.root
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.3.0
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++. IN NS
++SECTION ANSWER
++. IN NS NS.ROOT.
++SECTION ADDITIONAL
++NS.ROOT. IN A 1.2.3.0
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++mesa. IN NS
++SECTION AUTHORITY
++mesa. IN NS ns.mesa.
++SECTION ADDITIONAL
++ns.mesa. IN A 1.2.7.7
++ENTRY_END
++RANGE_END
++
++; ns.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.7.7
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++pollute1.mesa. IN NS
++SECTION AUTHORITY
++pollute1.mesa. IN NS ns.pollute1.mesa.
++SECTION ADDITIONAL
++ns.pollute1.mesa. IN A 1.2.4.1
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++pollute2.mesa. IN NS
++SECTION AUTHORITY
++pollute2.mesa. IN NS ns.pollute2.mesa.
++SECTION ADDITIONAL
++ns.pollute2.mesa. IN A 1.2.4.2
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++pollute3.mesa. IN NS
++SECTION AUTHORITY
++pollute3.mesa. IN NS ns.pollute3.mesa.
++SECTION ADDITIONAL
++ns.pollute3.mesa. IN A 1.2.4.3
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++attacker.mesa. IN NS
++SECTION AUTHORITY
++attacker.mesa. IN NS ns.attacker.mesa.
++SECTION ADDITIONAL
++ns.attacker.mesa. IN A 5.6.7.8
++ENTRY_END
++RANGE_END
++
++; ns.pollute1.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.4.1
++
++; This is the spoofed answer that is returned.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++test1.atkr.pollute1.mesa. IN A
++SECTION ANSWER
++test1.atkr.pollute1.mesa. 86400 IN A 1.2.3.4
++SECTION AUTHORITY
++pollute1.mesa. 86400 IN NS ns.attacker.mesa.
++ENTRY_END
++
++; correct answer for the check query.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute1.mesa. IN A
++SECTION ANSWER
++check.pollute1.mesa. IN A 1.8.9.1
++ENTRY_END
++RANGE_END
++
++; ns.pollute2.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.4.2
++
++; This is the spoofed answer that is returned.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++test2.atkr.pollute2.mesa. IN A
++SECTION ANSWER
++test2.atkr.pollute2.mesa. 86400 IN A 1.2.3.4
++SECTION AUTHORITY
++pollute2.mesa. 86400 IN NS ns.pollute2.mesa.
++SECTION ADDITIONAL
++ns.pollute2.mesa. 86400 IN A 5.6.7.8
++ENTRY_END
++
++; correct answer for the check query.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute2.mesa. IN A
++SECTION ANSWER
++check.pollute2.mesa. IN A 1.8.9.2
++ENTRY_END
++RANGE_END
++
++; ns.pollute3.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.4.3
++
++; This is the spoofed answer that is returned.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++test3.atkr.pollute3.mesa. IN A
++SECTION ANSWER
++test3.atkr.pollute3.mesa. 86400 IN A 1.2.3.4
++SECTION AUTHORITY
++test3.atkr.pollute3.mesa. 86400 IN NS ns.pollute3.mesa.
++SECTION ADDITIONAL
++ns.pollute3.mesa. 86400 IN A 5.6.7.8
++ENTRY_END
++
++; correct answer for the check query.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute3.mesa. IN A
++SECTION ANSWER
++check.pollute3.mesa. IN A 1.8.9.3
++ENTRY_END
++RANGE_END
++
++; ns.attacker.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 5.6.7.8
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++ns.attacker.mesa. IN A
++SECTION ANSWER
++ns.attacker.mesa. 86400 IN A 5.6.7.8
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++ns.attacker.mesa. IN AAAA
++SECTION AUTHORITY
++attacker.mesa. 3600 IN SOA ns.attacker.mesa. root.attacker.mesa. 4 7200 3600 604800 3600
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++ns.attacker.mesa. IN A
++SECTION ANSWER
++ns.attacker.mesa. 86400 IN A 5.6.7.8
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute1.mesa. IN A
++SECTION ANSWER
++check.pollute1.mesa. 86400 IN A 5.6.7.9
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute2.mesa. IN A
++SECTION ANSWER
++check.pollute2.mesa. 86400 IN A 5.6.7.9
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute3.mesa. IN A
++SECTION ANSWER
++check.pollute3.mesa. 86400 IN A 5.6.7.9
++ENTRY_END
++RANGE_END
++
++; Test query 1
++STEP 1 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++test1.atkr.pollute1.mesa. IN A
++ENTRY_END
++
++STEP 10 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++test1.atkr.pollute1.mesa. IN A
++SECTION ANSWER
++test1.atkr.pollute1.mesa. 86400 IN A 1.2.3.4
++ENTRY_END
++
++; Test query 2
++STEP 20 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++test2.atkr.pollute2.mesa. IN A
++ENTRY_END
++
++STEP 30 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++test2.atkr.pollute2.mesa. IN A
++SECTION ANSWER
++test2.atkr.pollute2.mesa. 86400 IN A 1.2.3.4
++ENTRY_END
++
++; Test query 3
++STEP 40 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++test3.atkr.pollute3.mesa. IN A
++ENTRY_END
++
++STEP 50 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++test3.atkr.pollute3.mesa. IN A
++SECTION ANSWER
++test3.atkr.pollute3.mesa. 86400 IN A 1.2.3.4
++ENTRY_END
++
++; Check the cache contents, for query 1.
++STEP 60 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++check.pollute1.mesa. IN A
++ENTRY_END
++
++STEP 70 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++check.pollute1.mesa. IN A
++SECTION ANSWER
++; good answer
++check.pollute1.mesa. IN A 1.8.9.1
++; bad answer
++;check.pollute1.mesa. IN A 5.6.7.9
++ENTRY_END
++
++; Check the cache contents, for query 2.
++STEP 80 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++check.pollute2.mesa. IN A
++ENTRY_END
++
++STEP 90 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++check.pollute2.mesa. IN A
++SECTION ANSWER
++; good answer
++check.pollute2.mesa. IN A 1.8.9.2
++; bad answer
++;check.pollute2.mesa. IN A 5.6.7.9
++ENTRY_END
++
++; Check the cache contents, for query 3.
++STEP 100 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++check.pollute3.mesa. IN A
++ENTRY_END
++
++STEP 110 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++check.pollute3.mesa. IN A
++SECTION ANSWER
++; good answer
++check.pollute3.mesa. IN A 1.8.9.3
++; bad answer
++;check.pollute3.mesa. IN A 5.6.7.9
++ENTRY_END
++
++SCENARIO_END
+diff --git a/testdata/iter_soamin.rpl b/testdata/iter_soamin.rpl
+index 7e902601b..0facc3508 100644
+--- a/testdata/iter_soamin.rpl
++++ b/testdata/iter_soamin.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_stub_noroot.rpl b/testdata/iter_stub_noroot.rpl
+index ef306bd42..749462b6e 100644
+--- a/testdata/iter_stub_noroot.rpl
++++ b/testdata/iter_stub_noroot.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_stubfirst.rpl b/testdata/iter_stubfirst.rpl
+index 1a7112de4..7cd3305a9 100644
+--- a/testdata/iter_stubfirst.rpl
++++ b/testdata/iter_stubfirst.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/iter_timeout_ra_aaaa.rpl b/testdata/iter_timeout_ra_aaaa.rpl
+index 126867ba4..9456f0420 100644
+--- a/testdata/iter_timeout_ra_aaaa.rpl
++++ b/testdata/iter_timeout_ra_aaaa.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/rrset_rettl.rpl b/testdata/rrset_rettl.rpl
+index 55dd62386..131a98e71 100644
+--- a/testdata/rrset_rettl.rpl
++++ b/testdata/rrset_rettl.rpl
+@@ -2,6 +2,7 @@
+ ; config options go here.
+ server:
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ forward-zone: name: "." forward-addr: 216.0.0.1
+ CONFIG_END
+
+diff --git a/testdata/rrset_untrusted.rpl b/testdata/rrset_untrusted.rpl
+index 6370ebf49..207275b56 100644
+--- a/testdata/rrset_untrusted.rpl
++++ b/testdata/rrset_untrusted.rpl
+@@ -2,6 +2,7 @@
+ ; config options go here.
+ server:
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ forward-zone: name: "." forward-addr: 216.0.0.1
+ CONFIG_END
+
+diff --git a/testdata/rrset_updated.rpl b/testdata/rrset_updated.rpl
+index 55da56bac..ba8e4924c 100644
+--- a/testdata/rrset_updated.rpl
++++ b/testdata/rrset_updated.rpl
+@@ -2,6 +2,7 @@
+ ; config options go here.
+ server:
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+ forward-zone: name: "." forward-addr: 216.0.0.1
+ CONFIG_END
+diff --git a/testdata/serve_expired.rpl b/testdata/serve_expired.rpl
+index 3f61019fa..2bba0d974 100644
+--- a/testdata/serve_expired.rpl
++++ b/testdata/serve_expired.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ access-control: 127.0.0.1/32 allow_snoop
+ ede: yes
+diff --git a/testdata/serve_expired_cached_servfail.rpl b/testdata/serve_expired_cached_servfail.rpl
+index 286de708b..1a18ff1c1 100644
+--- a/testdata/serve_expired_cached_servfail.rpl
++++ b/testdata/serve_expired_cached_servfail.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-reply-ttl: 123
+ log-servfail: yes
+diff --git a/testdata/serve_expired_client_timeout.rpl b/testdata/serve_expired_client_timeout.rpl
+index 5560aa05a..e40e1b4c3 100644
+--- a/testdata/serve_expired_client_timeout.rpl
++++ b/testdata/serve_expired_client_timeout.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-client-timeout: 1
+ serve-expired-reply-ttl: 123
+diff --git a/testdata/serve_expired_client_timeout_no_prefetch.rpl b/testdata/serve_expired_client_timeout_no_prefetch.rpl
+index aed397d9e..3a35c4629 100644
+--- a/testdata/serve_expired_client_timeout_no_prefetch.rpl
++++ b/testdata/serve_expired_client_timeout_no_prefetch.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-client-timeout: 1
+ serve-expired-reply-ttl: 123
+diff --git a/testdata/serve_expired_client_timeout_servfail.rpl b/testdata/serve_expired_client_timeout_servfail.rpl
+index 1cae3fd82..928360147 100644
+--- a/testdata/serve_expired_client_timeout_servfail.rpl
++++ b/testdata/serve_expired_client_timeout_servfail.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-client-timeout: 1
+ serve-expired-reply-ttl: 123
+diff --git a/testdata/serve_expired_reply_ttl.rpl b/testdata/serve_expired_reply_ttl.rpl
+index 124fb874d..063aad92b 100644
+--- a/testdata/serve_expired_reply_ttl.rpl
++++ b/testdata/serve_expired_reply_ttl.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-reply-ttl: 123
+ ede: yes
+diff --git a/testdata/serve_expired_ttl.rpl b/testdata/serve_expired_ttl.rpl
+index df4ecb89d..df3cd9082 100644
+--- a/testdata/serve_expired_ttl.rpl
++++ b/testdata/serve_expired_ttl.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-ttl: 10
+
+diff --git a/testdata/serve_expired_ttl_client_timeout.rpl b/testdata/serve_expired_ttl_client_timeout.rpl
+index 169d070ea..f28579014 100644
+--- a/testdata/serve_expired_ttl_client_timeout.rpl
++++ b/testdata/serve_expired_ttl_client_timeout.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-ttl: 10
+ serve-expired-client-timeout: 1
+diff --git a/testdata/serve_expired_zerottl.rpl b/testdata/serve_expired_zerottl.rpl
+index 0239b4a19..fbb76f96b 100644
+--- a/testdata/serve_expired_zerottl.rpl
++++ b/testdata/serve_expired_zerottl.rpl
+@@ -3,6 +3,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ serve-expired-reply-ttl: 123
+ ede: yes
+diff --git a/testdata/serve_original_ttl.rpl b/testdata/serve_original_ttl.rpl
+index 24d01b6fe..ced0672e9 100644
+--- a/testdata/serve_original_ttl.rpl
++++ b/testdata/serve_original_ttl.rpl
+@@ -4,6 +4,7 @@ server:
+ module-config: "validator iterator"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-original-ttl: yes
+ cache-max-ttl: 1000
+ cache-min-ttl: 20
+diff --git a/testdata/subnet_cached.crpl b/testdata/subnet_cached.crpl
+index 209831335..8f3c3de56 100644
+--- a/testdata/subnet_cached.crpl
++++ b/testdata/subnet_cached.crpl
+@@ -15,6 +15,7 @@ server:
+ access-control: 127.0.0.1 allow_snoop
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/subnet_cached_servfail.crpl b/testdata/subnet_cached_servfail.crpl
+index 9c746d579..535671b03 100644
+--- a/testdata/subnet_cached_servfail.crpl
++++ b/testdata/subnet_cached_servfail.crpl
+@@ -11,6 +11,7 @@ server:
+ access-control: 127.0.0.1 allow_snoop
+ qname-minimisation: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ serve-expired: yes
+ prefetch: yes
+
+diff --git a/testdata/subnet_max_source.crpl b/testdata/subnet_max_source.crpl
+index f5c7464ed..f3f71e7fd 100644
+--- a/testdata/subnet_max_source.crpl
++++ b/testdata/subnet_max_source.crpl
+@@ -11,6 +11,7 @@ server:
+ verbosity: 3
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/subnet_prefetch.crpl b/testdata/subnet_prefetch.crpl
+index 04922f2bb..79ef7bcf4 100644
+--- a/testdata/subnet_prefetch.crpl
++++ b/testdata/subnet_prefetch.crpl
+@@ -12,6 +12,7 @@ server:
+ access-control: 127.0.0.1 allow_snoop
+ qname-minimisation: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ prefetch: yes
+
+ stub-zone:
+diff --git a/testdata/subnet_prefetch_with_client_ecs.crpl b/testdata/subnet_prefetch_with_client_ecs.crpl
+index ddc832c47..8589db7e1 100644
+--- a/testdata/subnet_prefetch_with_client_ecs.crpl
++++ b/testdata/subnet_prefetch_with_client_ecs.crpl
+@@ -12,6 +12,7 @@ server:
+ access-control: 127.0.0.1 allow_snoop
+ qname-minimisation: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ prefetch: yes
+
+ stub-zone:
+diff --git a/testdata/subnet_val_positive.crpl b/testdata/subnet_val_positive.crpl
+index 01456e58b..10996ada8 100644
+--- a/testdata/subnet_val_positive.crpl
++++ b/testdata/subnet_val_positive.crpl
+@@ -13,6 +13,7 @@ server:
+ fake-dsa: yes
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/subnet_val_positive_client.crpl b/testdata/subnet_val_positive_client.crpl
+index b573742b7..1b51d52ef 100644
+--- a/testdata/subnet_val_positive_client.crpl
++++ b/testdata/subnet_val_positive_client.crpl
+@@ -14,6 +14,7 @@ server:
+ fake-dsa: yes
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/trust_cname_chain.rpl b/testdata/trust_cname_chain.rpl
+index f8415ba23..e24f8c10d 100644
+--- a/testdata/trust_cname_chain.rpl
++++ b/testdata/trust_cname_chain.rpl
+@@ -2,6 +2,7 @@
+ server:
+ target-fetch-policy: "0 0 0 0 0"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ stub-zone:
+ name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+diff --git a/testdata/ttl_max.rpl b/testdata/ttl_max.rpl
+index 325696321..b24eea383 100644
+--- a/testdata/ttl_max.rpl
++++ b/testdata/ttl_max.rpl
+@@ -4,6 +4,7 @@ server:
+ cache-max-ttl: 10
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/ttl_min.rpl b/testdata/ttl_min.rpl
+index 3c79ff5ed..94206c7c5 100644
+--- a/testdata/ttl_min.rpl
++++ b/testdata/ttl_min.rpl
+@@ -4,6 +4,7 @@ server:
+ cache-min-ttl: 10
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_adbit.rpl b/testdata/val_adbit.rpl
+index 7ce62de77..233c58bef 100644
+--- a/testdata/val_adbit.rpl
++++ b/testdata/val_adbit.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_adcopy.rpl b/testdata/val_adcopy.rpl
+index 604fd57f2..7bc31df23 100644
+--- a/testdata/val_adcopy.rpl
++++ b/testdata/val_adcopy.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_cnametocnamewctoposwc.rpl b/testdata/val_cnametocnamewctoposwc.rpl
+index c290026ba..6ad1d3809 100644
+--- a/testdata/val_cnametocnamewctoposwc.rpl
++++ b/testdata/val_cnametocnamewctoposwc.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ trust-anchor-signaling: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_ds_afterprime.rpl b/testdata/val_ds_afterprime.rpl
+index 3b1c0d614..301a1f6b6 100644
+--- a/testdata/val_ds_afterprime.rpl
++++ b/testdata/val_ds_afterprime.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_faildnskey_ok.rpl b/testdata/val_faildnskey_ok.rpl
+index d3ac00c47..d4d208417 100644
+--- a/testdata/val_faildnskey_ok.rpl
++++ b/testdata/val_faildnskey_ok.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_keyprefetch_verify.rpl b/testdata/val_keyprefetch_verify.rpl
+index 9b901a8cb..6cf81848d 100644
+--- a/testdata/val_keyprefetch_verify.rpl
++++ b/testdata/val_keyprefetch_verify.rpl
+@@ -10,6 +10,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_noadwhennodo.rpl b/testdata/val_noadwhennodo.rpl
+index 46e1bad5a..dbdeb780e 100644
+--- a/testdata/val_noadwhennodo.rpl
++++ b/testdata/val_noadwhennodo.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_nsec3_b3_optout.rpl b/testdata/val_nsec3_b3_optout.rpl
+index 9d84be974..5d8a43a9b 100644
+--- a/testdata/val_nsec3_b3_optout.rpl
++++ b/testdata/val_nsec3_b3_optout.rpl
+@@ -7,6 +7,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/val_nsec3_b3_optout_negcache.rpl b/testdata/val_nsec3_b3_optout_negcache.rpl
+index 497a8591a..e7be762fb 100644
+--- a/testdata/val_nsec3_b3_optout_negcache.rpl
++++ b/testdata/val_nsec3_b3_optout_negcache.rpl
+@@ -7,6 +7,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/val_nsec3_b4_wild.rpl b/testdata/val_nsec3_b4_wild.rpl
+index 8bf3a5466..295932fad 100644
+--- a/testdata/val_nsec3_b4_wild.rpl
++++ b/testdata/val_nsec3_b4_wild.rpl
+@@ -6,6 +6,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ trust-anchor-signaling: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/val_nsec3_cnametocnamewctoposwc.rpl b/testdata/val_nsec3_cnametocnamewctoposwc.rpl
+index 0fba0e2e1..8bcf8aefc 100644
+--- a/testdata/val_nsec3_cnametocnamewctoposwc.rpl
++++ b/testdata/val_nsec3_cnametocnamewctoposwc.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ trust-anchor-signaling: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_positive.rpl b/testdata/val_positive.rpl
+index daaf36089..c80851703 100644
+--- a/testdata/val_positive.rpl
++++ b/testdata/val_positive.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_positive_wc.rpl b/testdata/val_positive_wc.rpl
+index 5384acf63..591dcc603 100644
+--- a/testdata/val_positive_wc.rpl
++++ b/testdata/val_positive_wc.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ trust-anchor-signaling: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_qds_badanc.rpl b/testdata/val_qds_badanc.rpl
+index dc686153f..cb53136f6 100644
+--- a/testdata/val_qds_badanc.rpl
++++ b/testdata/val_qds_badanc.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_qds_oneanc.rpl b/testdata/val_qds_oneanc.rpl
+index f21ab422b..bda9f9032 100644
+--- a/testdata/val_qds_oneanc.rpl
++++ b/testdata/val_qds_oneanc.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_qds_twoanc.rpl b/testdata/val_qds_twoanc.rpl
+index 4e4f2e732..f801c023b 100644
+--- a/testdata/val_qds_twoanc.rpl
++++ b/testdata/val_qds_twoanc.rpl
+@@ -9,6 +9,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_refer_unsignadd.rpl b/testdata/val_refer_unsignadd.rpl
+index 4d073016f..22f15d21a 100644
+--- a/testdata/val_refer_unsignadd.rpl
++++ b/testdata/val_refer_unsignadd.rpl
+@@ -9,6 +9,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ trust-anchor-signaling: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/val_referd.rpl b/testdata/val_referd.rpl
+index d475f835e..a25ca7b7d 100644
+--- a/testdata/val_referd.rpl
++++ b/testdata/val_referd.rpl
+@@ -10,6 +10,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_referglue.rpl b/testdata/val_referglue.rpl
+index 54b767156..3ca0c0e80 100644
+--- a/testdata/val_referglue.rpl
++++ b/testdata/val_referglue.rpl
+@@ -10,6 +10,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ stub-zone:
+diff --git a/testdata/val_rrsig.rpl b/testdata/val_rrsig.rpl
+index 0b672e0f2..69df344a5 100644
+--- a/testdata/val_rrsig.rpl
++++ b/testdata/val_rrsig.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_spurious_ns.rpl b/testdata/val_spurious_ns.rpl
+index cb0a6e529..8db94a108 100644
+--- a/testdata/val_spurious_ns.rpl
++++ b/testdata/val_spurious_ns.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_stub_noroot.rpl b/testdata/val_stub_noroot.rpl
+index 07113bef7..66c3d8e88 100644
+--- a/testdata/val_stub_noroot.rpl
++++ b/testdata/val_stub_noroot.rpl
+@@ -6,6 +6,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_ta_algo_dnskey.rpl b/testdata/val_ta_algo_dnskey.rpl
+index 03bac83aa..5b0b64d25 100644
+--- a/testdata/val_ta_algo_dnskey.rpl
++++ b/testdata/val_ta_algo_dnskey.rpl
+@@ -9,6 +9,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_ta_algo_dnskey_dp.rpl b/testdata/val_ta_algo_dnskey_dp.rpl
+index 2b3609be8..ae0c499ca 100644
+--- a/testdata/val_ta_algo_dnskey_dp.rpl
++++ b/testdata/val_ta_algo_dnskey_dp.rpl
+@@ -10,6 +10,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_ta_algo_missing_dp.rpl b/testdata/val_ta_algo_missing_dp.rpl
+index dc55a09da..14efdeccb 100644
+--- a/testdata/val_ta_algo_missing_dp.rpl
++++ b/testdata/val_ta_algo_missing_dp.rpl
+@@ -11,6 +11,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_twocname.rpl b/testdata/val_twocname.rpl
+index bc7c3bcb2..b4323644a 100644
+--- a/testdata/val_twocname.rpl
++++ b/testdata/val_twocname.rpl
+@@ -5,6 +5,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+ rrset-roundrobin: no
+
+ forward-zone:
+diff --git a/testdata/val_unalgo_anchor.rpl b/testdata/val_unalgo_anchor.rpl
+index fbbf288a5..a93520122 100644
+--- a/testdata/val_unalgo_anchor.rpl
++++ b/testdata/val_unalgo_anchor.rpl
+@@ -7,6 +7,7 @@ server:
+ qname-minimisation: "no"
+ fake-sha1: yes
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/val_wild_pos.rpl b/testdata/val_wild_pos.rpl
+index 624d8e07b..9fafa6554 100644
+--- a/testdata/val_wild_pos.rpl
++++ b/testdata/val_wild_pos.rpl
+@@ -8,6 +8,7 @@ server:
+ fake-sha1: yes
+ trust-anchor-signaling: no
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ stub-zone:
+ name: "."
+diff --git a/testdata/views.rpl b/testdata/views.rpl
+index 6a9052fbe..a6026244b 100644
+--- a/testdata/views.rpl
++++ b/testdata/views.rpl
+@@ -3,6 +3,7 @@ server:
+ target-fetch-policy: "0 0 0 0 0"
+ qname-minimisation: "no"
+ minimal-responses: no
++ iter-scrub-promiscuous: no
+
+ access-control: 10.10.10.0/24 allow
+ access-control-view: 10.10.10.10/32 "view1"
+diff --git a/util/config_file.c b/util/config_file.c
+index f3e071059..2550d0b87 100644
+--- a/util/config_file.c
++++ b/util/config_file.c
+@@ -386,6 +386,7 @@ config_create(void)
+ cfg->ipset_name_v6 = NULL;
+ #endif
+ cfg->ede = 0;
++ cfg->iter_scrub_promiscuous = 1;
+ return cfg;
+ error_exit:
+ config_delete(cfg);
+@@ -690,6 +691,7 @@ int config_set_option(struct config_file* cfg, const char* opt,
+ else S_NUMBER_OR_ZERO("serve-expired-client-timeout:", serve_expired_client_timeout)
+ else S_YNO("ede:", ede)
+ else S_YNO("ede-serve-expired:", ede_serve_expired)
++ else S_YNO("iter-scrub-promiscuous:", iter_scrub_promiscuous)
+ else S_YNO("serve-original-ttl:", serve_original_ttl)
+ else S_STR("val-nsec3-keysize-iterations:", val_nsec3_key_iterations)
+ else S_YNO("zonemd-permissive-mode:", zonemd_permissive_mode)
+@@ -1141,6 +1143,7 @@ config_get_option(struct config_file* cfg, const char* opt,
+ else O_DEC(opt, "serve-expired-client-timeout", serve_expired_client_timeout)
+ else O_YNO(opt, "ede", ede)
+ else O_YNO(opt, "ede-serve-expired", ede_serve_expired)
++ else O_YNO(opt, "iter-scrub-promiscuous", iter_scrub_promiscuous)
+ else O_YNO(opt, "serve-original-ttl", serve_original_ttl)
+ else O_STR(opt, "val-nsec3-keysize-iterations",val_nsec3_key_iterations)
+ else O_YNO(opt, "zonemd-permissive-mode", zonemd_permissive_mode)
+diff --git a/util/config_file.h b/util/config_file.h
+index f933df2b2..26c28de84 100644
+--- a/util/config_file.h
++++ b/util/config_file.h
+@@ -714,6 +714,9 @@ struct config_file {
+ #endif
+ /** respond with Extended DNS Errors (RFC8914) */
+ int ede;
++ /** Should the iterator scrub promiscuous NS rrsets, from positive
++ * answers. */
++ int iter_scrub_promiscuous;
+ };
+
+ /** from cfg username, after daemonize setup performed */
+diff --git a/util/configlexer.lex b/util/configlexer.lex
+index b35666450..003a610e3 100644
+--- a/util/configlexer.lex
++++ b/util/configlexer.lex
+@@ -569,6 +569,7 @@ edns-client-string-opcode{COLON} { YDVAR(1, VAR_EDNS_CLIENT_STRING_OPCODE) }
+ nsid{COLON} { YDVAR(1, VAR_NSID ) }
+ ede{COLON} { YDVAR(1, VAR_EDE ) }
+ proxy-protocol-port{COLON} { YDVAR(1, VAR_PROXY_PROTOCOL_PORT) }
++iter-scrub-promiscuous{COLON} { YDVAR(1, VAR_ITER_SCRUB_PROMISCUOUS) }
+ <INITIAL,val>{NEWLINE} { LEXOUT(("NL\n")); cfg_parser->line++; }
+
+ /* Quoted strings. Strip leading and ending quotes */
+diff --git a/util/configparser.y b/util/configparser.y
+index 8c69b6aff..c8b83b7f8 100644
+--- a/util/configparser.y
++++ b/util/configparser.y
+@@ -196,6 +196,7 @@ extern struct config_parser_state* cfg_parser;
+ %token VAR_INTERFACE_ACTION VAR_INTERFACE_VIEW VAR_INTERFACE_TAG
+ %token VAR_INTERFACE_TAG_ACTION VAR_INTERFACE_TAG_DATA
+ %token VAR_PROXY_PROTOCOL_PORT VAR_STATISTICS_INHIBIT_ZERO
++%token VAR_ITER_SCRUB_PROMISCUOUS
+
+ %%
+ toplevelvars: /* empty */ | toplevelvars toplevelvar ;
+@@ -328,6 +329,7 @@ content_server: server_num_threads | server_verbosity | server_port |
+ server_tcp_reuse_timeout | server_tcp_auth_query_timeout |
+ server_interface_automatic_ports | server_ede |
+ server_proxy_protocol_port | server_statistics_inhibit_zero
++ | server_iter_scrub_promiscuous
+ ;
+ stubstart: VAR_STUB_ZONE
+ {
+@@ -3782,6 +3784,16 @@ server_tcp_connection_limit: VAR_TCP_CONNECTION_LIMIT STRING_ARG STRING_ARG
+ }
+ }
+ ;
++server_iter_scrub_promiscuous: VAR_ITER_SCRUB_PROMISCUOUS STRING_ARG
++ {
++ OUTYY(("P(server_iter_scrub_promiscuous:%s)\n", $2));
++ if(strcmp($2, "yes") != 0 && strcmp($2, "no") != 0)
++ yyerror("expected yes or no.");
++ else cfg_parser->cfg->iter_scrub_promiscuous =
++ (strcmp($2, "yes")==0);
++ free($2);
++ }
++ ;
+ ipsetstart: VAR_IPSET
+ {
+ OUTYY(("\nP(ipset:)\n"));
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch unbound-1.17.1/debian/patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch
--- unbound-1.17.1/debian/patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,264 @@
+From: Yorgos Thessalonikefs <yorgos@nlnetlabs.nl>
+Date: Wed, 26 Nov 2025 11:09:40 +0100
+Subject: Additional fix for CVE-2025-11411 (possible domain hijacking attack)
+
+Fix to include YXDOMAIN and non-referral nodata answers in the mitigation as
+well, reported by TaoFei Guo from Peking University, Yang Luo and JianJun Chen
+from Tsinghua University.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/f6269baa605d31859f28770e01a24e3677e5f82c
+Bug: https://nlnetlabs.nl/downloads/unbound/CVE-2023-50387.txt
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2023-50387
+Bug-Debian: https://bugs.debian.org/1121446
+---
+ iterator/iter_scrub.c | 39 +++++++++--
+ testdata/iter_scrub_promiscuous.rpl | 84 ++++++++++++++++++++++++
+ testdata/ratelimit.tdir/ratelimit.testns | 28 ++++++--
+ 3 files changed, 143 insertions(+), 8 deletions(-)
+
+diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c
+index b7f91e145..2b1af0c7f 100644
+--- a/iterator/iter_scrub.c
++++ b/iterator/iter_scrub.c
+@@ -356,19 +356,21 @@ soa_in_auth(struct msg_parse* msg)
+ * @param qinfo: original query.
+ * @param region: where to allocate synthesized CNAMEs.
+ * @param env: module env with config options.
++ * @param zonename: name of server zone.
+ * @return 0 on error.
+ */
+ static int
+ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
+ struct query_info* qinfo, struct regional* region,
+- struct module_env* env)
++ struct module_env* env, uint8_t* zonename)
+ {
+ uint8_t* sname = qinfo->qname;
+ size_t snamelen = qinfo->qname_len;
+ struct rrset_parse* rrset, *prev, *nsset=NULL;
+
+ if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR &&
+- FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN)
++ FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN &&
++ FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_YXDOMAIN)
+ return 1;
+
+ /* For the ANSWER section, remove all "irrelevant" records and add
+@@ -397,6 +399,11 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
+ &aliaslen, pkt)) {
+ verbose(VERB_ALGO, "synthesized CNAME "
+ "too long");
++ if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) {
++ prev = rrset;
++ rrset = rrset->rrset_all_next;
++ continue;
++ }
+ return 0;
+ }
+ if(nx && nx->type == LDNS_RR_TYPE_CNAME &&
+@@ -558,6 +565,29 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg,
+ "RRset:", pkt, msg, prev, &rrset);
+ continue;
+ }
++ /* Also delete promiscuous NS for other RCODEs */
++ if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR
++ && env->cfg->iter_scrub_promiscuous) {
++ remove_rrset("normalize: removing promiscuous "
++ "RRset:", pkt, msg, prev, &rrset);
++ continue;
++ }
++ /* Also delete promiscuous NS for NOERROR with nodata
++ * for authoritative answers, not for delegations.
++ * NOERROR with an_rrsets!=0 already handled.
++ * Also NOERROR and soa_in_auth already handled.
++ * NOERROR with an_rrsets==0, and not a referral.
++ * referral is (NS not the zonename, noSOA).
++ */
++ if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR
++ && msg->an_rrsets == 0
++ && !(dname_pkt_compare(pkt, rrset->dname,
++ zonename) != 0 && !soa_in_auth(msg))
++ && env->cfg->iter_scrub_promiscuous) {
++ remove_rrset("normalize: removing promiscuous "
++ "RRset:", pkt, msg, prev, &rrset);
++ continue;
++ }
+ if(nsset == NULL) {
+ nsset = rrset;
+ } else {
+@@ -850,7 +880,8 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
+ /* this is not required for basic operation but is a forgery
+ * resistance (security) feature */
+ if((FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR ||
+- FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN) &&
++ FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN ||
++ FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) &&
+ msg->qdcount == 0)
+ return 0;
+
+@@ -864,7 +895,7 @@ scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
+ }
+
+ /* normalize the response, this cleans up the additional. */
+- if(!scrub_normalize(pkt, msg, qinfo, region, env))
++ if(!scrub_normalize(pkt, msg, qinfo, region, env, zonename))
+ return 0;
+ /* delete all out-of-zone information */
+ if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie))
+diff --git a/testdata/iter_scrub_promiscuous.rpl b/testdata/iter_scrub_promiscuous.rpl
+index 61fca0d28..febbee81c 100644
+--- a/testdata/iter_scrub_promiscuous.rpl
++++ b/testdata/iter_scrub_promiscuous.rpl
+@@ -16,6 +16,7 @@ SCENARIO_BEGIN Test iterator with scrub of promiscuous records
+ ; The spoofed contents are ns.attacker.mesa and its IPs 5.6.7.8 and 5.6.7.9.
+ ; The pollute1.mesa NS, ns.pollute2.mesa A, and test3.atkr.pollute3.mesa NS
+ ; with ns.pollute3.mesa A records are tested for cache placement.
++; pollute4.mesa uses YXDOMAIN.
+
+ ; ns.root
+ RANGE_BEGIN 0 400
+@@ -84,6 +85,18 @@ SECTION ADDITIONAL
+ ns.pollute3.mesa. IN A 1.2.4.3
+ ENTRY_END
+
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++pollute4.mesa. IN NS
++SECTION AUTHORITY
++pollute4.mesa. IN NS ns.pollute4.mesa.
++SECTION ADDITIONAL
++ns.pollute4.mesa. IN A 1.2.4.4
++ENTRY_END
++
+ ENTRY_BEGIN
+ MATCH opcode subdomain
+ ADJUST copy_id copy_query
+@@ -188,6 +201,35 @@ check.pollute3.mesa. IN A 1.8.9.3
+ ENTRY_END
+ RANGE_END
+
++; ns.pollute4.mesa
++RANGE_BEGIN 0 400
++ ADDRESS 1.2.4.4
++
++; This is the spoofed answer that is returned.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA YXDOMAIN
++SECTION QUESTION
++test4.atkr.pollute4.mesa. IN A
++SECTION ANSWER
++test4.atkr.pollute4.mesa. 86400 IN A 1.2.3.4
++SECTION AUTHORITY
++pollute4.mesa. 86400 IN NS ns.attacker.mesa.
++ENTRY_END
++
++; correct answer for the check query.
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR AA NOERROR
++SECTION QUESTION
++check.pollute4.mesa. IN A
++SECTION ANSWER
++check.pollute4.mesa. IN A 1.8.9.4
++ENTRY_END
++RANGE_END
++
+ ; ns.attacker.mesa
+ RANGE_BEGIN 0 400
+ ADDRESS 5.6.7.8
+@@ -370,4 +412,46 @@ check.pollute3.mesa. IN A 1.8.9.3
+ ;check.pollute3.mesa. IN A 5.6.7.9
+ ENTRY_END
+
++; Test query 4
++STEP 120 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++test4.atkr.pollute4.mesa. IN A
++ENTRY_END
++
++STEP 130 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA YXDOMAIN
++SECTION QUESTION
++test4.atkr.pollute4.mesa. IN A
++SECTION ANSWER
++test4.atkr.pollute4.mesa. 86400 IN A 1.2.3.4
++SECTION AUTHORITY
++; removed record
++;pollute4.mesa. 0 IN NS ns.attacker.mesa.
++ENTRY_END
++
++; Check the cache contents, for query 4.
++STEP 140 QUERY
++ENTRY_BEGIN
++REPLY RD
++SECTION QUESTION
++check.pollute4.mesa. IN A
++ENTRY_END
++
++STEP 150 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA NOERROR
++SECTION QUESTION
++check.pollute4.mesa. IN A
++SECTION ANSWER
++; good answer
++check.pollute4.mesa. IN A 1.8.9.4
++; bad answer
++;check.pollute4.mesa. IN A 5.6.7.9
++ENTRY_END
++
+ SCENARIO_END
+diff --git a/testdata/ratelimit.tdir/ratelimit.testns b/testdata/ratelimit.tdir/ratelimit.testns
+index 673bd15a5..5c22c292d 100644
+--- a/testdata/ratelimit.tdir/ratelimit.testns
++++ b/testdata/ratelimit.tdir/ratelimit.testns
+@@ -3,11 +3,31 @@ $ORIGIN example.com.
+ $TTL 3600
+
+ ENTRY_BEGIN
+-MATCH opcode qtype
++MATCH opcode qname qtype
+ REPLY QR AA NOERROR
+-ADJUST copy_id copy_query
++ADJUST copy_id
+ SECTION QUESTION
+-wild IN A
++www1 IN A
+ SECTION ANSWER
+-wild IN A 10.20.30.40
++www1 IN A 1.1.1.1
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qname qtype
++REPLY QR AA NOERROR
++ADJUST copy_id
++SECTION QUESTION
++www2 IN A
++SECTION ANSWER
++www2 IN A 2.2.2.2
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode qname qtype
++REPLY QR AA NOERROR
++ADJUST copy_id
++SECTION QUESTION
++www3 IN A
++SECTION ANSWER
++www3 IN A 3.3.3.3
+ ENTRY_END
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch unbound-1.17.1/debian/patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch
--- unbound-1.17.1/debian/patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch 2025-11-30 13:19:21.000000000 +0300
@@ -0,0 +1,148 @@
+From: Yorgos Thessalonikefs <yorgos@nlnetlabs.nl>
+Date: Mon, 8 Apr 2024 14:15:03 +0200
+Subject: Fix #595: unbound-anchor cannot deal with full disk
+
+- Fix #595: unbound-anchor cannot deal with full disk; it will now
+ first write out to a temp file before replacing the original one,
+ like Unbound already does for auto-trust-anchor-file.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/8575d5b35ce3b91b41962fbba69030cc8789c3bf
+Bug: https://github.com/NLnetLabs/unbound/issues/595
+Bug-Debian: https://bugs.debian.org/1100870
+---
+ smallapp/unbound-anchor.c | 70 ++++++++++++++++++++++++++-------------
+ 1 file changed, 47 insertions(+), 23 deletions(-)
+
+diff --git a/smallapp/unbound-anchor.c b/smallapp/unbound-anchor.c
+index 3bc25a10c..a8330f3a3 100644
+--- a/smallapp/unbound-anchor.c
++++ b/smallapp/unbound-anchor.c
+@@ -1836,15 +1836,49 @@ verify_p7sig(BIO* data, BIO* p7s, STACK_OF(X509)* trust, const char* p7signer)
+ return secure;
+ }
+
++/** open a temp file */
++static FILE*
++tempfile_open(char* tempf, size_t tempflen, const char* fname, const char* mode)
++{
++ snprintf(tempf, tempflen, "%s~", fname);
++ return fopen(tempf, mode);
++}
++
++/** close an open temp file and replace the original with it */
++static void
++tempfile_close(FILE* fd, const char* tempf, const char* fname)
++{
++ fflush(fd);
++#ifdef HAVE_FSYNC
++ fsync(fileno(fd));
++#else
++ FlushFileBuffers((HANDLE)_get_osfhandle(_fileno(fd)));
++#endif
++ if(fclose(fd) != 0) {
++ printf("could not complete write: %s: %s\n",
++ tempf, strerror(errno));
++ unlink(tempf);
++ return;
++ }
++ /* success; overwrite actual file */
++#ifdef USE_WINSOCK
++ (void)unlink(fname); /* windows does not replace file with rename() */
++#endif
++ if(rename(tempf, fname) < 0) {
++ printf("rename(%s to %s): %s", tempf, fname, strerror(errno));
++ }
++}
++
+ /** write unsigned root anchor file, a 5011 revoked tp */
+ static void
+ write_unsigned_root(const char* root_anchor_file)
+ {
+ FILE* out;
+ time_t now = time(NULL);
+- out = fopen(root_anchor_file, "w");
++ char tempf[2048];
++ out = tempfile_open(tempf, sizeof(tempf), root_anchor_file, "w");
+ if(!out) {
+- if(verb) printf("%s: %s\n", root_anchor_file, strerror(errno));
++ if(verb) printf("%s: %s\n", tempf, strerror(errno));
+ return;
+ }
+ if(fprintf(out, "; autotrust trust anchor file\n"
+@@ -1859,13 +1893,7 @@ write_unsigned_root(const char* root_anchor_file)
+ root_anchor_file);
+ if(verb && errno != 0) printf("%s\n", strerror(errno));
+ }
+- fflush(out);
+-#ifdef HAVE_FSYNC
+- fsync(fileno(out));
+-#else
+- FlushFileBuffers((HANDLE)_get_osfhandle(_fileno(out)));
+-#endif
+- fclose(out);
++ tempfile_close(out, tempf, root_anchor_file);
+ }
+
+ /** write root anchor file */
+@@ -1875,29 +1903,24 @@ write_root_anchor(const char* root_anchor_file, BIO* ds)
+ char* pp = NULL;
+ int len;
+ FILE* out;
++ char tempf[2048];
+ (void)BIO_seek(ds, 0);
+ len = BIO_get_mem_data(ds, &pp);
+ if(!len || !pp) {
+ if(verb) printf("out of memory\n");
+ return;
+ }
+- out = fopen(root_anchor_file, "w");
++ out = tempfile_open(tempf, sizeof(tempf), root_anchor_file, "w");
+ if(!out) {
+- if(verb) printf("%s: %s\n", root_anchor_file, strerror(errno));
++ if(verb) printf("%s: %s\n", tempf, strerror(errno));
+ return;
+ }
+ if(fwrite(pp, (size_t)len, 1, out) != 1) {
+ if(verb) printf("failed to write all data to %s\n",
+- root_anchor_file);
++ tempf);
+ if(verb && errno != 0) printf("%s\n", strerror(errno));
+ }
+- fflush(out);
+-#ifdef HAVE_FSYNC
+- fsync(fileno(out));
+-#else
+- FlushFileBuffers((HANDLE)_get_osfhandle(_fileno(out)));
+-#endif
+- fclose(out);
++ tempfile_close(out, tempf, root_anchor_file);
+ }
+
+ /** Perform the verification and update of the trustanchor file */
+@@ -2041,18 +2064,19 @@ try_read_anchor(const char* file)
+ static void
+ write_builtin_anchor(const char* file)
+ {
++ char tempf[2048];
+ const char* builtin_root_anchor = get_builtin_ds();
+- FILE* out = fopen(file, "w");
++ FILE* out = tempfile_open(tempf, sizeof(tempf), file, "w");
+ if(!out) {
+ printf("could not write builtin anchor, to file %s: %s\n",
+- file, strerror(errno));
++ tempf, strerror(errno));
+ return;
+ }
+ if(!fwrite(builtin_root_anchor, strlen(builtin_root_anchor), 1, out)) {
+ printf("could not complete write builtin anchor, to file %s: %s\n",
+- file, strerror(errno));
++ tempf, strerror(errno));
+ }
+- fclose(out);
++ tempfile_close(out, tempf, file);
+ }
+
+ /**
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch unbound-1.17.1/debian/patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch
--- unbound-1.17.1/debian/patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch 2025-11-30 10:33:46.000000000 +0300
@@ -0,0 +1,292 @@
+From: "W.C.A. Wijngaards" <wouter@nlnetlabs.nl>
+Date: Fri, 6 Jan 2023 09:16:59 +0100
+Subject: Fix #823: Response change to NODATA for some ANY queries since
+ 1.12, tested on 1.16.1.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/ba6325f24f6462420d3adf80a3c21848ab8e9fe0
+Bug: https://github.com/NLnetLabs/unbound/issues/823
+Comment: backported to 1.17 by Michael Tokarev <mjt@tls.msk.ru>
+---
+ doc/Changelog | 4 +
+ testdata/val_any_negcache.rpl | 241 ++++++++++++++++++++++++++++++++++
+ validator/val_neg.c | 5 +
+ 3 files changed, 250 insertions(+)
+
+diff --git a/doc/Changelog b/doc/Changelog
+index 899026352..13f7b475c 100644
+--- a/doc/Changelog
++++ b/doc/Changelog
+@@ -1,3 +1,7 @@
++6 January 2023: Wouter
++ - Fix #823: Response change to NODATA for some ANY queries since
++ 1.12, tested on 1.16.1.
++
+ 5 January 2023: Wouter
+ - Tag for 1.17.1 release.
+
+diff --git a/testdata/val_any_negcache.rpl b/testdata/val_any_negcache.rpl
+new file mode 100644
+index 000000000..662f0634a
+--- /dev/null
++++ b/testdata/val_any_negcache.rpl
+@@ -0,0 +1,241 @@
++; config options
++; The island of trust is at example.com
++server:
++ trust-anchor: "example.com. 3600 IN DS 2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
++ val-override-date: "20070916134226"
++ target-fetch-policy: "0 0 0 0 0"
++ qname-minimisation: "no"
++ fake-sha1: yes
++ trust-anchor-signaling: no
++ rrset-roundrobin: no
++ aggressive-nsec: yes
++
++stub-zone:
++ name: "."
++ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
++CONFIG_END
++
++SCENARIO_BEGIN Test validator with response to qtype ANY and negative cache.
++
++; K.ROOT-SERVERS.NET.
++RANGE_BEGIN 0 100
++ ADDRESS 193.0.14.129
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++. IN NS
++SECTION ANSWER
++. IN NS K.ROOT-SERVERS.NET.
++SECTION ADDITIONAL
++K.ROOT-SERVERS.NET. IN A 193.0.14.129
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++com. IN NS
++SECTION AUTHORITY
++com. IN NS a.gtld-servers.net.
++SECTION ADDITIONAL
++a.gtld-servers.net. IN A 192.5.6.30
++ENTRY_END
++RANGE_END
++
++; a.gtld-servers.net.
++RANGE_BEGIN 0 100
++ ADDRESS 192.5.6.30
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++com. IN NS
++SECTION ANSWER
++com. IN NS a.gtld-servers.net.
++SECTION ADDITIONAL
++a.gtld-servers.net. IN A 192.5.6.30
++ENTRY_END
++
++ENTRY_BEGIN
++MATCH opcode subdomain
++ADJUST copy_id copy_query
++REPLY QR NOERROR
++SECTION QUESTION
++example.com. IN NS
++SECTION AUTHORITY
++example.com. IN NS ns.example.com.
++SECTION ADDITIONAL
++ns.example.com. IN A 1.2.3.4
++ENTRY_END
++RANGE_END
++
++; ns.example.com.
++RANGE_BEGIN 0 100
++ ADDRESS 1.2.3.4
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++example.com. IN NS
++SECTION ANSWER
++example.com. IN NS ns.example.com.
++example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854}
++SECTION ADDITIONAL
++ns.example.com. IN A 1.2.3.4
++ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926135752 20070829135752 2854 example.com. MC0CFQCMSWxVehgOQLoYclB9PIAbNP229AIUeH0vNNGJhjnZiqgIOKvs1EhzqAo= ;{id = 2854}
++ENTRY_END
++
++; response to DNSKEY priming query
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++example.com. IN DNSKEY
++SECTION ANSWER
++example.com. 3600 IN DNSKEY 256 3 3 ALXLUsWqUrY3JYER3T4TBJII s70j+sDS/UT2QRp61SE7S3E EXopNXoFE73JLRmvpi/UrOO/Vz4Se 6wXv/CYCKjGw06U4WRgR YXcpEhJROyNapmdIKSx hOzfLVE1gqA0PweZR8d tY3aNQSRn3sPpwJr6Mi /PqQKAMMrZ9ckJpf1+b QMOOvxgzz2U1GS18b3y ZKcgTMEaJzd/GZYzi/B N2DzQ0MsrSwYXfsNLFO Bbs8PJMW4LYIxeeOe6rUgkWOF 7CC9Dh/dduQ1QrsJhmZAEFfd6ByYV+ ;{id = 2854 (zsk), size = 1688b}
++example.com. 3600 IN RRSIG DNSKEY 3 2 3600 20070926134802 20070829134802 2854 example.com. MCwCFG1yhRNtTEa3Eno2zhVVuy2EJX3wAhQeLyUp6+UXcpC5qGNu9tkrTEgPUg== ;{id = 2854}
++SECTION AUTHORITY
++example.com. IN NS ns.example.com.
++example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854}
++SECTION ADDITIONAL
++ns.example.com. IN A 1.2.3.4
++ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926135752 20070829135752 2854 example.com. MC0CFQCMSWxVehgOQLoYclB9PIAbNP229AIUeH0vNNGJhjnZiqgIOKvs1EhzqAo= ;{id = 2854}
++ENTRY_END
++
++; response with NODATA
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++example.com. IN LOC
++SECTION AUTHORITY
++example.com. 86400 IN SOA open.example.com. hostmaster.example.com. 2007090400 28800 7200 604800 18000
++example.com. 86400 IN RRSIG SOA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCSs8KJepwaIp5vu++/0hk04lkXvgIUdphJSAE/MYob30WcRei9/nL49tE= ;{id = 2854}
++example.com. 18000 IN NSEC _sip._udp.example.com. A NS SOA MX TXT AAAA NAPTR RRSIG NSEC DNSKEY
++example.com. 18000 IN RRSIG NSEC 3 2 18000 20070926134150 20070829134150 2854 example.com. MCwCFBzOGtpgq4uJ2jeuLPYl2HowIRzDAhQVXNz1haQ1mI7z9lt5gcvWW+lFhA== ;{id = 2854}
++ENTRY_END
++
++; response to query of interest
++ENTRY_BEGIN
++MATCH opcode qtype qname
++ADJUST copy_id
++REPLY QR NOERROR
++SECTION QUESTION
++example.com. IN ANY
++SECTION ANSWER
++example.com. 86400 IN SOA open.example.com. hostmaster.example.com. 2007090400 28800 7200 604800 18000
++example.com. 86400 IN RRSIG SOA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCSs8KJepwaIp5vu++/0hk04lkXvgIUdphJSAE/MYob30WcRei9/nL49tE= ;{id = 2854}
++example.com. 3600 IN DNSKEY 256 3 3 ALXLUsWqUrY3JYER3T4TBJIIs70j+sDS/UT2QRp61SE7S3EEXopNXoFE73JLRmvpi/UrOO/Vz4Se6wXv/CYCKjGw06U4WRgRYXcpEhJROyNapmdIKSxhOzfLVE1gqA0PweZR8dtY3aNQSRn3sPpwJr6Mi/PqQKAMMrZ9ckJpf1+bQMOOvxgzz2U1GS18b3yZKcgTMEaJzd/GZYzi/BN2DzQ0MsrSwYXfsNLFOBbs8PJMW4LYIxeeOe6rUgkWOF7CC9Dh/dduQ1QrsJhmZAEFfd6ByYV+ ;{id = 2854 (zsk), size = 1688b}
++example.com. 3600 IN RRSIG DNSKEY 3 2 3600 20070926134150 20070829134150 2854 example.com. MCwCFHq7BNVAeLW+Uw/rkjVS08lrMDk/AhR+bvChHfiE4jLb6uoyE54/irCuqA== ;{id = 2854}
++example.com. 600 IN NAPTR 20 0 "s" "SIP+D2U" "" _sip._udp.example.com.
++example.com. 600 IN RRSIG NAPTR 3 2 600 20070926134150 20070829134150 2854 example.com. MC0CFE8qs66bzuOyKmTIacamrmqabMRzAhUAn0MujX1LB0UpTHuLMgdgMgJJlq4= ;{id = 2854}
++example.com. 86400 IN AAAA 2001:7b8:206:1::1
++example.com. 86400 IN RRSIG AAAA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFEqS4WHyqhUkv7t42TsBZJk/Q9paAhUAtTZ8GaXGpot0PmsM0oGzQU+2iw4= ;{id = 2854}
++example.com. 86400 IN TXT "Stichting NLnet Labs"
++example.com. 86400 IN RRSIG TXT 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFH3otn2u8zXczBS8L0VKpyAYZGSkAhQLGaQclkzMAzlB5j73opFjdkh8TA== ;{id = 2854}
++example.com. 86400 IN MX 100 v.net.example.
++example.com. 86400 IN MX 50 open.example.com.
++example.com. 86400 IN RRSIG MX 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFEKh3jeqh69zcOqWWv3GNKlMECPyAhR9HJkcPLqlyVWUccWDFJfGGcQfdg== ;{id = 2854}
++example.com. 86400 IN NS v.net.example.
++example.com. 86400 IN NS open.example.com.
++example.com. 86400 IN NS ns7.domain-registry.example.
++example.com. 86400 IN RRSIG NS 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCaRn30X4neKW7KYoTa2kcsoOLgfgIURvKEyDczLypWlx99KpxzMxRYhEc= ;{id = 2854}
++example.com. 86400 IN A 213.154.224.1
++example.com. 86400 IN RRSIG A 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFH8kSLxmRTwzlGDxvF1e4y/gM+5dAhQkzyQ2a6Gf+CMaHzVScaUvTt9HhQ== ;{id = 2854}
++example.com. 18000 IN NSEC _sip._udp.example.com. A NS SOA MX TXT AAAA NAPTR RRSIG NSEC DNSKEY
++example.com. 18000 IN RRSIG NSEC 3 2 18000 20070926134150 20070829134150 2854 example.com. MCwCFBzOGtpgq4uJ2jeuLPYl2HowIRzDAhQVXNz1haQ1mI7z9lt5gcvWW+lFhA== ;{id = 2854}
++SECTION AUTHORITY
++SECTION ADDITIONAL
++ns7.domain-registry.example. 80173 IN A 62.4.86.230
++open.example.com. 600 IN A 213.154.224.1
++open.example.com. 600 IN AAAA 2001:7b8:206:1::53
++open.example.com. 600 IN AAAA 2001:7b8:206:1::1
++v.net.example. 28800 IN A 213.154.224.17
++v.net.example. 28800 IN AAAA 2001:7b8:206:1:200:39ff:fe59:b187
++johnny.example.com. 600 IN A 213.154.224.44
++open.example.com. 600 IN RRSIG A 3 3 600 20070926134150 20070829134150 2854 example.com. MC0CFQCh8bja923UJmg1+sYXMK8WIE4dpgIUQe9sZa0GOcUYSgb2rXoogF8af+Y= ;{id = 2854}
++open.example.com. 600 IN RRSIG AAAA 3 3 600 20070926134150 20070829134150 2854 example.com. MC0CFQCRGJgIS6kEVG7aJfovuG/q3cgOWwIUYEIFCnfRQlMIYWF7BKMQoMbdkE0= ;{id = 2854}
++johnny.example.com. 600 IN RRSIG A 3 3 600 20070926134150 20070829134150 2854 example.com. MCwCFAh0/zSpCd/9eMNz7AyfnuGQFD1ZAhQEpNFNw4XByNEcbi/vsVeii9kp7g== ;{id = 2854}
++_sip._udp.example.com. 600 IN RRSIG SRV 3 4 600 20070926134150 20070829134150 2854 example.com. MCwCFFSRVgOcq1ihVuO6MhCuzWs6SxpVAhRPHHCKy0JxymVkYeFOxTkbVSWMMw== ;{id = 2854}
++_sip._udp.example.com. 600 IN SRV 0 0 5060 johnny.example.com.
++ENTRY_END
++RANGE_END
++
++STEP 1 QUERY
++ENTRY_BEGIN
++MATCH TCP
++REPLY RD DO
++SECTION QUESTION
++example.com. IN LOC
++ENTRY_END
++
++STEP 10 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA AD DO NOERROR
++SECTION QUESTION
++example.com. IN LOC
++SECTION ANSWER
++SECTION AUTHORITY
++example.com. 86400 IN SOA open.example.com. hostmaster.example.com. 2007090400 28800 7200 604800 18000
++example.com. 86400 IN RRSIG SOA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCSs8KJepwaIp5vu++/0hk04lkXvgIUdphJSAE/MYob30WcRei9/nL49tE= ;{id = 2854}
++example.com. 18000 IN NSEC _sip._udp.example.com. A NS SOA MX TXT AAAA NAPTR RRSIG NSEC DNSKEY
++example.com. 18000 IN RRSIG NSEC 3 2 18000 20070926134150 20070829134150 2854 example.com. MCwCFBzOGtpgq4uJ2jeuLPYl2HowIRzDAhQVXNz1haQ1mI7z9lt5gcvWW+lFhA== ;{id = 2854}
++ENTRY_END
++
++STEP 20 QUERY
++ENTRY_BEGIN
++MATCH TCP
++REPLY RD DO
++SECTION QUESTION
++example.com. IN ANY
++ENTRY_END
++
++; recursion happens here.
++STEP 30 CHECK_ANSWER
++ENTRY_BEGIN
++MATCH all
++REPLY QR RD RA AD DO NOERROR
++SECTION QUESTION
++example.com. IN ANY
++SECTION ANSWER
++example.com. 86400 IN SOA open.example.com. hostmaster.example.com. 2007090400 28800 7200 604800 18000
++example.com. 86400 IN RRSIG SOA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCSs8KJepwaIp5vu++/0hk04lkXvgIUdphJSAE/MYob30WcRei9/nL49tE= ;{id = 2854}
++example.com. 3600 IN DNSKEY 256 3 3 ALXLUsWqUrY3JYER3T4TBJIIs70j+sDS/UT2QRp61SE7S3EEXopNXoFE73JLRmvpi/UrOO/Vz4Se6wXv/CYCKjGw06U4WRgRYXcpEhJROyNapmdIKSxhOzfLVE1gqA0PweZR8dtY3aNQSRn3sPpwJr6Mi/PqQKAMMrZ9ckJpf1+bQMOOvxgzz2U1GS18b3yZKcgTMEaJzd/GZYzi/BN2DzQ0MsrSwYXfsNLFOBbs8PJMW4LYIxeeOe6rUgkWOF7CC9Dh/dduQ1QrsJhmZAEFfd6ByYV+ ;{id = 2854 (zsk), size = 1688b}
++example.com. 3600 IN RRSIG DNSKEY 3 2 3600 20070926134150 20070829134150 2854 example.com. MCwCFHq7BNVAeLW+Uw/rkjVS08lrMDk/AhR+bvChHfiE4jLb6uoyE54/irCuqA== ;{id = 2854}
++example.com. 600 IN NAPTR 20 0 "s" "SIP+D2U" "" _sip._udp.example.com.
++example.com. 600 IN RRSIG NAPTR 3 2 600 20070926134150 20070829134150 2854 example.com. MC0CFE8qs66bzuOyKmTIacamrmqabMRzAhUAn0MujX1LB0UpTHuLMgdgMgJJlq4= ;{id = 2854}
++example.com. 86400 IN AAAA 2001:7b8:206:1::1
++example.com. 86400 IN RRSIG AAAA 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFEqS4WHyqhUkv7t42TsBZJk/Q9paAhUAtTZ8GaXGpot0PmsM0oGzQU+2iw4= ;{id = 2854}
++example.com. 86400 IN TXT "Stichting NLnet Labs"
++example.com. 86400 IN RRSIG TXT 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFH3otn2u8zXczBS8L0VKpyAYZGSkAhQLGaQclkzMAzlB5j73opFjdkh8TA== ;{id = 2854}
++example.com. 86400 IN MX 100 v.net.example.
++example.com. 86400 IN MX 50 open.example.com.
++example.com. 86400 IN RRSIG MX 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFEKh3jeqh69zcOqWWv3GNKlMECPyAhR9HJkcPLqlyVWUccWDFJfGGcQfdg== ;{id = 2854}
++example.com. 86400 IN NS v.net.example.
++example.com. 86400 IN NS open.example.com.
++example.com. 86400 IN NS ns7.domain-registry.example.
++example.com. 86400 IN RRSIG NS 3 2 86400 20070926134150 20070829134150 2854 example.com. MC0CFQCaRn30X4neKW7KYoTa2kcsoOLgfgIURvKEyDczLypWlx99KpxzMxRYhEc= ;{id = 2854}
++example.com. 86400 IN A 213.154.224.1
++example.com. 86400 IN RRSIG A 3 2 86400 20070926134150 20070829134150 2854 example.com. MCwCFH8kSLxmRTwzlGDxvF1e4y/gM+5dAhQkzyQ2a6Gf+CMaHzVScaUvTt9HhQ== ;{id = 2854}
++example.com. 18000 IN NSEC _sip._udp.example.com. A NS SOA MX TXT AAAA NAPTR RRSIG NSEC DNSKEY
++example.com. 18000 IN RRSIG NSEC 3 2 18000 20070926134150 20070829134150 2854 example.com. MCwCFBzOGtpgq4uJ2jeuLPYl2HowIRzDAhQVXNz1haQ1mI7z9lt5gcvWW+lFhA== ;{id = 2854}
++SECTION AUTHORITY
++SECTION ADDITIONAL
++open.example.com. 600 IN A 213.154.224.1
++open.example.com. 600 IN AAAA 2001:7b8:206:1::53
++open.example.com. 600 IN AAAA 2001:7b8:206:1::1
++_sip._udp.example.com. 600 IN SRV 0 0 5060 johnny.example.com.
++open.example.com. 600 IN RRSIG A 3 3 600 20070926134150 20070829134150 2854 example.com. MC0CFQCh8bja923UJmg1+sYXMK8WIE4dpgIUQe9sZa0GOcUYSgb2rXoogF8af+Y= ;{id = 2854}
++open.example.com. 600 IN RRSIG AAAA 3 3 600 20070926134150 20070829134150 2854 example.com. MC0CFQCRGJgIS6kEVG7aJfovuG/q3cgOWwIUYEIFCnfRQlMIYWF7BKMQoMbdkE0= ;{id = 2854}
++_sip._udp.example.com. 600 IN RRSIG SRV 3 4 600 20070926134150 20070829134150 2854 example.com. MCwCFFSRVgOcq1ihVuO6MhCuzWs6SxpVAhRPHHCKy0JxymVkYeFOxTkbVSWMMw== ;{id = 2854}
++ENTRY_END
++
++SCENARIO_END
+diff --git a/validator/val_neg.c b/validator/val_neg.c
+index 67699b1f7..6990e9a06 100644
+--- a/validator/val_neg.c
++++ b/validator/val_neg.c
+@@ -1407,6 +1407,11 @@ val_neg_getmsg(struct val_neg_cache* neg, struct query_info* qinfo,
+ /* Matching NSEC, use to generate No Data answer. Not creating answers
+ * yet for No Data proven using wildcard. */
+ if(nsec && nsec_proves_nodata(nsec, qinfo, &nodata_wc) && !nodata_wc) {
++ /* do not create nodata answers for qtype ANY, it is a query
++ * type, not an rrtype to disprove. Nameerrors are useful for
++ * qtype ANY, in the else branch. */
++ if(qinfo->qtype == LDNS_RR_TYPE_ANY)
++ return NULL;
+ if(!(msg = dns_msg_create(qinfo->qname, qinfo->qname_len,
+ qinfo->qtype, qinfo->qclass, region, 2)))
+ return NULL;
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/fix-not-following-cleared-RD-flags-amplification.patch unbound-1.17.1/debian/patches/fix-not-following-cleared-RD-flags-amplification.patch
--- unbound-1.17.1/debian/patches/fix-not-following-cleared-RD-flags-amplification.patch 1970-01-01 03:00:00.000000000 +0300
+++ unbound-1.17.1/debian/patches/fix-not-following-cleared-RD-flags-amplification.patch 2025-11-30 13:19:19.000000000 +0300
@@ -0,0 +1,54 @@
+From: "W.C.A. Wijngaards" <wouter@nlnetlabs.nl>
+Date: Wed, 18 Jan 2023 13:18:47 +0100
+Subject: Fix not following cleared RD flags potentially enables amplification
+ DDoS attacks, reported by Xiang Li and Wei Xu from NISL Lab,
+ Tsinghua University. The fix stops query loops, by refusing to send
+ RD=0 queries to a forwarder, they still get answered from cache.
+
+Origin: https://github.com/NLnetLabs/unbound/commit/b12ab31ae36ae2b124748d37835d74dca15b161f
+---
+ doc/Changelog | 6 ++++++
+ iterator/iterator.c | 13 +++++++++++++
+ 2 files changed, 19 insertions(+)
+
+diff --git a/doc/Changelog b/doc/Changelog
+index 13f7b475c..ecbb04be1 100644
+--- a/doc/Changelog
++++ b/doc/Changelog
+@@ -1,3 +1,9 @@
++18 January 2023: Wouter
++ - Fix not following cleared RD flags potentially enables amplification
++ DDoS attacks, reported by Xiang Li and Wei Xu from NISL Lab,
++ Tsinghua University. The fix stops query loops, by refusing to send
++ RD=0 queries to a forwarder, they still get answered from cache.
++
+ 6 January 2023: Wouter
+ - Fix #823: Response change to NODATA for some ANY queries since
+ 1.12, tested on 1.16.1.
+diff --git a/iterator/iterator.c b/iterator/iterator.c
+index 33095b2b5..751179496 100644
+--- a/iterator/iterator.c
++++ b/iterator/iterator.c
+@@ -1451,6 +1451,19 @@ processInitRequest(struct module_qstate* qstate, struct iter_qstate* iq,
+ errinf(qstate, "malloc failure for forward zone");
+ return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
+ }
++ if((qstate->query_flags&BIT_RD)==0) {
++ /* If the server accepts RD=0 queries and forwards
++ * with RD=1, then if the server is listed as an NS
++ * entry, it starts query loops. Stop that loop by
++ * disallowing the query. The RD=0 was previously used
++ * to check the cache with allow_snoop. For stubs,
++ * the iterator pass would have primed the stub and
++ * then cached information can be used for further
++ * queries. */
++ verbose(VERB_ALGO, "cannot forward RD=0 query, to stop query loops");
++ errinf(qstate, "cannot forward RD=0 query");
++ return error_response(qstate, id, LDNS_RCODE_SERVFAIL);
++ }
+ iq->refetch_glue = 0;
+ iq->minimisation_state = DONOT_MINIMISE_STATE;
+ /* the request has been forwarded.
+--
+2.47.3
+
diff -Nru unbound-1.17.1/debian/patches/series unbound-1.17.1/debian/patches/series
--- unbound-1.17.1/debian/patches/series 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/patches/series 2025-11-30 13:19:21.000000000 +0300
@@ -2,7 +2,10 @@
do-not-chown-control-socket.patch
do-not-look-at-pidfile.patch
fix-812-fix-846-by-using-the-SSL_OP_IGNORE_UNEXPECTE.patch
-CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch
+fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch
+fix-not-following-cleared-RD-flags-amplification.patch
+CVE-2023-50387-DNSSEC-verification-complexity.patch
+CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch
CVE-2024-43168/01-193401e75.patch
CVE-2024-43168/02-dfff8d23c.patch
CVE-2024-43168/03-4497e8a15.patch
@@ -15,3 +18,7 @@
CVE-2024-33655.patch
CVE-2025-5994.patch
0017-Updated-IPv4-and-IPv6-address-for-b.root-servers.net.patch
+CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch
+CVE-2025-11411/2-possible-domain-hijacking-attack.patch
+CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch
+fix-595-unbound-anchor-cannot-deal-with-full-disk.patch
diff -Nru unbound-1.17.1/debian/salsa-ci.yml unbound-1.17.1/debian/salsa-ci.yml
--- unbound-1.17.1/debian/salsa-ci.yml 2025-08-24 19:37:35.000000000 +0300
+++ unbound-1.17.1/debian/salsa-ci.yml 2025-11-29 13:14:16.000000000 +0300
@@ -9,3 +9,5 @@
SALSA_CI_DISABLE_AUTOPKGTEST: 1
SALSA_CI_DISABLE_BUILD_PACKAGE_ALL: 1
SALSA_CI_DISABLE_BUILD_PACKAGE_ANY: 1
+ SALSA_CI_DISABLE_LINTIAN: 1
+ SALSA_CI_DISABLE_REPROTEST: 1
Reply to: