Version in base suite: 1.17.1-2+deb12u3 Base version: unbound_1.17.1-2+deb12u3 Target version: unbound_1.17.1-2+deb12u4 Base file: /srv/ftp-master.debian.org/ftp/pool/main/u/unbound/unbound_1.17.1-2+deb12u3.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/u/unbound/unbound_1.17.1-2+deb12u4.dsc changelog | 27 gbp.conf | 3 patches/CVE-2023-50387-DNSSEC-verification-complexity.patch | 782 +++ patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch | 2299 ---------- patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch | 1750 +++++++ patches/CVE-2024-33655.patch | 15 patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch | 44 patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch | 2167 +++++++++ patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch | 264 + patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch | 148 patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch | 292 + patches/fix-not-following-cleared-RD-flags-amplification.patch | 54 patches/series | 9 salsa-ci.yml | 2 14 files changed, 5542 insertions(+), 2314 deletions(-) diff -Nru unbound-1.17.1/debian/changelog unbound-1.17.1/debian/changelog --- unbound-1.17.1/debian/changelog 2025-08-24 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/changelog 2025-11-30 10:33:55.000000000 +0000 @@ -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 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 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/gbp.conf 2025-11-30 10:33:55.000000000 +0000 @@ -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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2023-50387-DNSSEC-verification-complexity.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,782 @@ +From: "W.C.A. Wijngaards" +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; ientry.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 + #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; inow, 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; ian_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+1msg_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; ian_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; + irrset_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 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2023-50387_CVE-2023-50868_1.16.1-1.17.1.patch 1970-01-01 00:00:00.000000000 +0000 @@ -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; irep->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: 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; ientry.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 - #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; inow, 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; ian_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+1msg_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; ian_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; - irrset_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; ian_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; ian_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; ian_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; ian_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; ian_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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2023-50868-NSEC3-closest-encloser-proof-exhaust-CPU.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,1750 @@ +From: "W.C.A. Wijngaards" +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; irep->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: 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; ian_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; ian_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; ian_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; ian_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; ian_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 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2024-33655.patch 2025-11-30 10:33:55.000000000 +0000 @@ -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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2025-11411/1-iterator-iter_scrub.c-pass-module_env-parameter-to-s.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,44 @@ +From: Michael Tokarev +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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2025-11411/2-possible-domain-hijacking-attack.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,2167 @@ +From: Yorgos Thessalonikefs +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 +--- + 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 ++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 + 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) } + {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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/CVE-2025-11411/3-additional-fix-for-possible-domain-hijacking.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,264 @@ +From: Yorgos Thessalonikefs +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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/fix-595-unbound-anchor-cannot-deal-with-full-disk.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,148 @@ +From: Yorgos Thessalonikefs +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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/fix-823-Response-change-to-NODATA-for-some-ANY-queries.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,292 @@ +From: "W.C.A. Wijngaards" +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 +--- + 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 00:00:00.000000000 +0000 +++ unbound-1.17.1/debian/patches/fix-not-following-cleared-RD-flags-amplification.patch 2025-11-30 10:33:55.000000000 +0000 @@ -0,0 +1,54 @@ +From: "W.C.A. Wijngaards" +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 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/patches/series 2025-11-30 10:33:55.000000000 +0000 @@ -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 16:37:35.000000000 +0000 +++ unbound-1.17.1/debian/salsa-ci.yml 2025-11-30 10:33:55.000000000 +0000 @@ -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