Index | Thread | Search

From:
Tom Smyth <tom.smyth@wirelessconnect.eu>
Subject:
Re: unbound 1.26.1
To:
tech <tech@openbsd.org>
Date:
Sun, 20 Sep 2026 13:06:18 +0100

Download raw body.

Thread
Thanks for doing this Stuart,
some nasty RCEs in there that are fixed
Much Obliged

On Sun, 20 Sept 2026 at 11:12, Stuart Henderson <stu@spacehopper.org> wrote:

> Index: doc/Changelog
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/Changelog,v
> diff -u -p -r1.57 Changelog
> --- doc/Changelog       26 May 2026 11:14:11 -0000      1.57
> +++ doc/Changelog       20 Sep 2026 09:50:48 -0000
> @@ -1,3 +1,507 @@
> +24 July 2026: Wouter
> +       - Merge #1433 from jisakiel: Add new static zone type
> +         block_aaaa to suppress AAAA queries.
> +       - Unit test for block_a and block_aaaa.
> +       - Fix #1477: respip + dns64: dns64 uses A records modified by
> +         respip instead of original A records. Adds local-zone types
> +         block_a_wdata and block_aaaa_wdata, that are like block_a
> +         and block_aaaa, and uses local-data if present.
> +       - set code repository version to 1.26.0.
> +       - Update generated man pages.
> +       - Fix to allow test fake sha1 on systems with possible sha1
> +         support.
> +       - Fix to use sha256 for unbound-anchor unit test.
> +       - Fix unbound-anchor check for return value of
> +         X509_NAME_get_text_by_NID of the emailaddress.
> +       - Fix lock test protect for auth zone change.
> +       - Fix to lock shared_ports structure during initialisation.
> +       - Fix to lock anchor structure when file is set for it in
> +         parse of the header.
> +       - Merge #1480 from petrvaganoff: authzone: fix memory leak in
> +         xfer_set_masters() error path.
> +       - Fix unused variable warnings in shared_ports_fetch_random
> +         and shared_ports_return_port when compiled without threads.
> +       - Fix to guard access to shared ports interface array during
> +         set up, for analyzer.
> +       - Fix sign of comparison warning in shared ports setup.
> +       - Fix #1481: Fix to use tls-port after referral if
> +         tls-upstream is set.
> +       - Merge #1479 from psumbera: Fix pthread detection on
> +         Solaris 11.4.
> +       - Fix to call OPENSSL_cleanup on exit when that is defined.
> +
> +23 July 2026: Wouter
> +       - Updated credits for Xuanchao Xie in 22 july changelog.
> +       - Merge #1478 from petrvaganoff: pythonmod: add check return
> +         value after ftell().
> +       - Fix that for NSEC3 proofs the NSEC3 zone, as the b32.name is
> +         checked to be the same as the signer name. Also RRSIGs are
> +         not considered valid when an NSEC3 is not b32.signerzone.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that the aggressive negative cache does not insert NSEC
> +         records with overreaching next owner name. Also the result
> +         is not above the trust anchor's bailiwick. Also RRSIGS are
> +         not considered valid when an NSEC next owner name is not
> +         under the signer zone name. Thanks to Qifan Zhang, Palo
> +         Alto Networks, for the report.
> +       - Fix mesh cycle detection for configuration with respip CNAME
> +         loop and tagged clients. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +
> +22 July 2026: Wouter
> +       - Release tag for 1.25.2, with the security commits:
> +       - Fix CVE-2026-14586, Assertion in libngtcp2 when under pressure
> +         in high concurrency DNS-over-QUIC environments. Thanks to Kunta
> +         Chu, Kaihua Wang, and Jianjun Chen from Tsinghua University,
> +         for the report.
> +       - Fix CVE-2026-32665, Remote DNS-over-QUIC denial of
> +         service due to `quic-size` budget bypass. Thanks to N0zoM1z0
> +         (https://github.com/N0zoM1z0) for the report. In addition,
> thanks to
> +         Kunta Chu, Kaihua Wang, and Jianjun Chen from Tsinghua
> University,
> +         for also reporting this issue. In addition, thanks to Qifan
> Zhang,
> +         Palo Alto Networks, for also reporting this issue. In addition,
> +         thanks to Xuanchao Xie, Lutong Chen, and Kaiping Xue of the
> +         University of Science and Technology of China (USTC), for also
> +         reporting this issue.
> +       - Fix CVE-2026-40691, Packet of death for DNSCrypt over TCP. Thanks
> +         to Qifan Zhang, Palo Alto Networks, for the report. In addition,
> +         thanks to Trung Nguyen (@everping) of CyStack, for also reporting
> +         this issue.
> +       - Fix CVE-2026-41637, Degradation of resolution service from
> +         improperly accounted client-terminated DNS-over-QUIC queries.
> Thanks
> +         to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix CVE-2026-42955, Extra fix for CVE-2026-40622 to also clamp
> +         the TTL of A/AAAA records disallowing a one-time 'ghost domain'
> +         delegation renewal via glue records. Thanks to Qifan Zhang, Palo
> +         Alto Networks, for the report.
> +       - Fix CVE-2026-44621, Libunbound applications configured with
> +         'unwanted-reply-threshold' could eventually be abruptly
> +         terminated. Thanks to Qifan Zhang, Palo Alto Networks, for the
> +         report.
> +       - Fix CVE-2026-44687, Off-by-one error in 'harden-below-nxdomain'
> +         logic can shadow a stub/forward zone by a legitimate parent's
> +         NXDOMAIN. Thanks to Qifan Zhang, Palo Alto Networks, for the
> report.
> +       - Fix CVE-2026-44690, Cross-zone wildcard cache poisoning via
> +         RRSIG.labels manipulation. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix CVE-2026-46582, A wildcard replay, as another piece of data,
> +         triggers poisoning in the serve expired reply path. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix CVE-2026-50045, 'max-global-quota' reset by DNSSEC validation
> +         restarts. Thanks to Kunjie Shang, University of Science and
> +         Technology of China, for the report.
> +       - Fix CVE-2026-50046, Possible heap use-after-free in an error path
> +         when a DoT forwarded query is jostled out. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix CVE-2026-50243, 'response-ip'/'rpz' can rewrite BOGUS answers
> +         instead of returning SERVFAIL. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix CVE-2026-50248, BOGUS configured primary hostname accepted
> for
> +         XFR in auth/rpz zones. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix CVE-2026-50251, Attacker supplied `0.0.0.0`/`::` glue
> triggers
> +         defensive full-cache flush. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix CVE-2026-50252, Possible cache poisoning attack by mapping
> +         source port population per thread. Thanks to Inbal Schussheim and
> +         Amit Klein, Hebrew University, for the report.
> +       - Fix CVE-2026-52863, Memory corruption could lead to crash and
> +         denial of service. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix CVE-2026-54478, DNS Cookie bypass when combined with
> +         proxy-protocol use. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix CVE-2026-55708, Privacy/configuration issue when adding local
> +         data in views through 'unbound-control'. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix CVE-2026-55717, 'serve-expired-client-timeout' and
> 'response-ip'
> +         CNAME redirect could lead to a crash. Thanks to Qifan Zhang, Palo
> +         Alto Networks, for the report. In addition, thanks to Xin Wang,
> +         Jiapeng Li, and Jiajia Liu, Northwestern Polytechnical
> University,
> +         for also reporting this issue.
> +       - Fix CVE-2026-55973, 'dns-error-reporting: yes' leads to stack
> buffer
> +         overflow. Thanks to Qifan Zhang, Palo Alto Networks, for the
> report.
> +       - Fix CVE-2026-55990, Packet of death for a DNSCrypt misconfigured
> +         Unbound. Thanks to Qifan Zhang, Palo Alto Networks, for the
> report.
> +       - Fix CVE-2026-55991, Remote DNS-over-QUIC (DoQ) flow-control
> +         assertion failure in libngtcp2. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report. In addition, thanks to Xuanchao Xie,
> +         Lutong Chen, and Kaiping Xue of the University of Science and
> +         Technology of China (USTC), for also reporting this issue.
> +       - Fix CVE-2026-56416, Possible heap buffer overflow when validator
> +         canonicalizes RDATA that contains domain name. Thanks to Qifan
> +         Zhang, Palo Alto Networks, for the report.
> +       - Fix CVE-2026-56444, Degradation of resolution service when
> +         'discard-timeout' and 'serve-expired-client-timeout' are
> combined in
> +         unusual configuration. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report. In addition, thanks to Xin Wang, Jiapeng Li,
> +         and Jiajia Liu, Northwestern Polytechnical University, for also
> +         reporting this issue. In addition, thanks to Haruki Oyama (Waseda
> +         University), for also reporting this issue.
> +       - Set the repository to 1.25.3, it continues with the previous
> +         changes.
> +       - Unit test for CVE-2026-42955.
> +       - Unit test for CVE-2026-44687.
> +       - Unit test for CVE-2026-44690.
> +       - Unit test for CVE-2026-46582.
> +       - Unit test for CVE-2026-50045.
> +       - Unit test for CVE-2026-50243.
> +       - Unit test for CVE-2026-50248.
> +       - Unit test for CVE-2026-55717.
> +       - Unit test for CVE-2026-55973.
> +       - Unit test for CVE-2026-56416.
> +       - Fix error in log printout in fix for CVE-2026-50248, when the
> +         primary name is bogus.
> +       - iana portlist update.
> +
> +21 July 2026: Wouter
> +       - Merge #1476 from petrvaganoff: ipsecmod: fix possible deref
> +         on null after reply_find_answer_rrset().
> +
> +20 July 2026: Wouter
> +       - Merge #1475 from petrvaganoff: ipsecmod: fix deref on null
> +         in ipsecmod-whitelist after OOM.
> +       - Fix #1474: DoQ responses are never padded - pad-responses
> +         does not apply to comm_doq (RFC 9250 §5.4 MUST).
> +
> +9 July 2026: Wouter
> +       - Merge #1383 from jdek: Fix randomness generation on
> +         macOS/iOS under chroot.
> +       - Fix unit test for malformed svcb for test on Windows.
> +
> +2 July 2026: Wouter
> +       - Merge #1087: Overload `local_data_remove` to support removing
> +         specific records.
> +
> +30 June 2026: Wouter
> +       - Fix #1469: dohclient: DoH POST missing content-length → :status
> +         400 from strict resolvers (Cloudflare, Mullvad).
> +       - iana portlist updated.
> +
> +26 June 2026: Wouter
> +       - Merge #1467: daemon: fix DEREF_AFTER_NULL.EX.COND on
> +         worker_init. This fixes error handling if the worker
> +         stat_timer allocation has an out of memory error. That
> +         makes the server not crash later, attempting to use it.
> +
> +24 June 2026: Wouter
> +       - Merge #1465 from dag-erling: Add libunbound/remote.h. Add
> +         a shared header containing prototypes for functions that
> +         both ends of a remote control connection need to implement.
> +
> +19 June 2026: Wouter
> +       - Fix for #1457: fix thread setname for thread start of
> +         dnstap, and fast_reload.
> +       - Fix to update github ci actions/checkout to v7.
> +       - Fix warning about file_string_matches in unbound-checkconf.
> +
> +17 June 2026: Wouter
> +       - Fix that after fast_reload the disown of the auth zone
> +         transfer task cleans the chunk list. Also fix the
> +         auth_transfer_limit test to use a forwarder for each type
> +         of failure, so the one is not blocked by the other waiting.
> +       - Fix to remove debug from auth_transfer_limit test.
> +       - Fix that unbound-checkconf checks if an auth-zone download
> +         can overwrite another file, by filename collision.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that malloc failure in auth-zone insert rr does
> +         not create an empty node and does not cause an infinite
> +         loop. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix that unbound-control auth_zone_reload stops the
> +         server answering from the zone after a failure to read.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that malloc failure in dns64_inform_super does
> +         not set up a half-built reply for cache store, that could
> +         lead to a crash. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix that malloc failure for new_local_rrset for RPZ qname
> +         trigger RR insert does not crash. It does not link a
> +         partial RRset, and logs an error on failure, and cleans
> +         up the dname allocation. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that malloc failure in doq connection setup, does
> +         not crash in doq connection delete later. Thanks to Qifan
> +         Zhang, Palo Alto Networks, for the report.
> +       - Fix that malloc failure for ngtcp2_conn_server_new
> +         cleans up reference that older ngtcp2 versions can leave.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that on malloc failure during accept of TCP, the
> +         socket is not left to cause a read event loop. It uses
> +         slow-accept to delay accepting new connections, if
> +         that fails it drops the new connections. When the tcp
> +         connection usage is full, it waits for 50msec, to allow
> +         existing queries to be resolved. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix that malloc failure for rpz_strip_nsdname is
> +         checked and handled, so that it does not crash later.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that malloc failure during edns subnet addrtree
> +         insert is checked, so it does not crash later. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix to check the return value of auth_xfer_create
> +         during fast_reload auth-zone add and change processing.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix to check for malloc failure in rpz response create,
> +         for nodata and nxdomain, so it does not crash later.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that fast_reload does not terminate the server
> +         on malloc failure for dnstap, or if gethostname fails.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix after malloc failure for stats, then it drains the pipe
> +         so the internal messaging stays correct. Also it does
> +         not exit the server if stats pipe communication fails.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that fast_reload does not terminate the server
> +         on config read failure after malloc failure. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that fast_reload does not terminate the server if
> +         random init for DNS cookies fails. The data is only random
> +         generated if cookies are enabled, and the random data
> +         is necessary. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +
> +17 June 2026: Yorgos
> +       - Fix memory leak on DNAME 0TTL records.
> +
> +16 June 2026: Wouter
> +       - Fix to disallow $INCLUDE for secondary zones. Start up
> +         of server continues if a secondary zone fails to load.
> +         Failed loads clear the zone data, so there is no partial
> +         zone. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix that when SVCB records cannot be written out, and
> +         are written in unknown format, that the zone read allows
> +         such unknown format SVCB records. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix that a half-written trust anchor file does not crash
> +         the server at runtime. It unlinks a wrong file from the list.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that auth-zone, and RPZ zones, do not allow out-of-zone
> +         records. These are records that are not under the zone apex.
> +         The out-of-zone records are dropped from the zone contents.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that dns64 does not ignore the `forward-no-cache` and
> +         `stub-no-cache` options. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that a signed wildcard NSEC, is checked before use,
> +         so it does not allow insecure DS proofs inappropriately.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that after malloc failure a half-built local_alias does
> +         not crash the server. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that for a zonefile only zone, if that file does not
> +         exist on server start, the server continues to start with
> +         a warning log message. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that after malloc failure in RPZ load a half built
> +         list does not crash later. The newly created RRset is
> +         linked after creation has succeeded. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix that dnscrypt configuration does not crash, due to
> +         inconsistency between secret and public keys. Also
> +         duplicate files are skipped. Thanks to Qifan Zhang, Palo
> +         Alto Networks, for the report.
> +       - Fix locking in libunbound ub_ctx_set_event call.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that libunbound pipe functions fail with error after
> +         an event base is set. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix for neater solution to clear log thread id after
> +         worker init failure. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix incorrect cleanup after an allocation failure for
> +         a delegation point. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that after malloc failure in find_tag_datas, the
> +         local_alias is cleaned up. Thanks to Qifan Zhang, Palo
> +         Alto Networks, for the report.
> +       - Fix that after shared memory cannot be created, from
> +         `shm-enable`, the server does not crash. Thanks to Qifan
> +         Zhang, Palo Alto Networks, for the report.
> +       - Fix incorrect cleanup after an allocation failure for
> +         a delegation point in a region. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix after malloc failure the rrset_insert_rr in
> +         localzone processing, during RPZ qname trigger processing,
> +         the RRset retains its previous data correcly. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix for #1462: Fix that auth primary host name lookup
> +         allows CNAMEs.
> +
> +15 June 2026: Wouter
> +       - Fix to add `max-transfer-size` and `max-transfer-time` that
> +         limit auth-zone and rpz transfer amount and time taken.
> +         Default is disabled. This hardens against unbounded
> +         transfers. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix perform a full transfer every number of incremental
> +         transfers, to stop increasing memory usage, for rpz
> +         zones. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix assertion failure for long HTTP header that fills
> +         buffer. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix buffer overflow when configured with lower than
> +         default size and http transfer. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix that misconfigured `iter-scrub-ns: 0` causes request
> +         failures. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix that fast_reload when a zonemd verification lookup
> +         it in progress with subnet loaded, deregisters the
> +         callback. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix for fast_reload that removes an auth zone while its
> +         lookups are in progress, for a primary name. Also after the
> +         change, it no longer picks up the old results. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix integer overflow in infra-cache-max-rtt calculation.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix erroneous DNS error report values after bogus AAAA
> +         query caused error information that was not cleared by
> +         a successful A subquery. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix integer overflow for very high values of
> +         `sock-queue-timeout`. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix that fast_reload does not terminate the server for
> +         errors in config, for key files. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix log of an aliased qname, to not use freed region
> +         memory. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix DNAME synthesis from cache that keeps use of 0TTL
> +         entries in a sliding window. It did not surpass RRSIG
> +         expiry. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix misconfigured ipsecmod hook causing path name
> +         similarity with other file. The ipsecmod is changed for
> +         exec of the hook. The ipsecmod hook, if a script, has to
> +         start now with a line like `#!/bin/sh`. Thanks to Qifan
> +         Zhang, Palo Alto Networks, for the report.
> +       - Fix that dns64 bypasses rpz-passthru rule during
> +         synthesis. This restricted more than necessary. Thanks to
> +         Qifan Zhang, Palo Alto Networks, for the report.
> +
> +12 June 2026: Wouter
> +       - Fix that for auth-zone and rpz zones the allow-notify
> +         addresses and netblocks are available from start, and
> +         fix the probe step skip.
> +
> +11 June 2026: Wouter
> +       - Fix for #1306: configure detects specifically the call to
> +         SSL_set_quic_tls_early_data_enabled and
> +         SSL_set_quic_early_data_enabled, so the correct one is used.
> +       - Fix for #1306: configure checks if the ngtcp2_crypto_ossl
> +         header file is available, and prints an error otherwise.
> +       - Fix #1437: Fix compile with OpenSSL 4.0.1.
> +       - Fix compile for OpenSSL 1.0.2 and before in server cleanup.
> +
> +10 June 2026: Wouter
> +       - Fix pythonmod script read for numeric overflow.
> +       - Fix warnings with gcc in compat/inet_pton.c.
> +
> +9 June 2026: Wouter
> +       - Fix unit test for ecs to check for malloc success.
> +
> +3 June 2026: Wouter
> +       - Fix that the processing of class responses does not have
> +         a heap use-after-free. That could happen if at least two
> +         distinct classes are configured for resolution. Thanks
> +         to Qifan Zhang, Palo Alto Networks for the report.
> +         In addition, thanks to Xin Wang, Jiapeng Li, and Jiajia
> +         Liu, Northwestern Polytechnical University, for also
> +         reporting this.
> +       - Fix negative cache to work with NSEC3 records without salt.
> +         Thanks to Xin Wang, Jiapeng Li, and Jiajia Liu, Northwestern
> +         Polytechnical University, for the report.
> +       - Fix parse of svcbparam ech, it had incorrect length. Thanks
> +         to Qifan Zhang, Palo Alto Networks for the report.
> +       - Fix that quotation and escaping works the same in auth-zone
> +         url content, as in the zonefile read. Thanks to Qifan Zhang,
> +         Palo Alto Networks for the report.
> +       - Fix ipset module to use larger domain name buffers, and
> +         check buffer lengths. Thanks to Qifan Zhang, Palo Alto
> +         Networks for the report.
> +       - Fix PROXYv2 header read and consume, it checks the header
> +         size. Thanks to Qifan Zhang, Palo Alto Networks for
> +         the report.
> +       - Fix negative cache NSEC3 nodata proof, to use the correct
> +         message size. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix fast_reload for when a ZONEMD lookup is in progress.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that validation canonicalization of domain names
> +         in rdata checks for buffer bounds. Thanks to Qifan Zhang,
> +         Palo Alto Networks, for the report.
> +       - Fix that dump_cache has a larger buffer for records,
> +         and it checks that an owner name does not collide with BADRR
> +         on the input, and changes verbosity on the log of failure in
> +         rrset to string.  Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix that dns64 cleans up the allocated message if the adjust
> +         routines fail, and checks if there is a reply before cache
> +         store, also unbound checks if A and AAAA are malformed
> +         for auth-zones. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +
> +3 June 2026: Yorgos
> +       - Fix const as reported by newest compiler warnings.
> +
> +29 May 2026: Wouter
> +       - Fix header_seen detection for trust anchor files, so that it
> +         detects the id line.
> +       - iana portlist updated.
> +       - Update icannbundle.pem certificates in unbound-anchor. It
> +         has the public keys for 2009 to 2029 and for 2025 to 2045.
> +       - Fix unit test to check for new icannbundle.pem.
> +
> +28 May 2026: Wouter
> +       - Fix #1457: race condition causes segfault when starting
> +         threads.
> +
> +27 May 2026: Wouter
> +       - Fix for autotrust state-file line overflow, that can give
> +         hold-down bypass. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix to limit the DSNS per-label walk in the iterator. Thanks
> +         to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that the ratelimit is decremented on successful
> +         referrals. Thanks to Qifan Zhang, Palo Alto Networks, for
> +         the report.
> +       - Fix that msgencode insert_query has the correct assertion,
> +         for a local_alias. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix to reset the tcp-timeout before applying a load based
> +         reduction. Thanks to Qifan Zhang, Palo Alto Networks, for the
> +         report.
> +       - Fix to decrement the per-netblock tcp connection limits, so
> +         it keeps usable. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix manual to document ratelimit, that it is for target
> +         nameservers for a domain, and keeps queries limited. Thanks
> +         to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix, in depth, for respip rewrite of dns64 responses. Thanks
> +         to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that dns64 with subnetcache does not write ECS scoped
> +         answers to global cache. Thanks to Qifan Zhang, Palo Alto
> +         Networks, for the report.
> +       - Fix ipset module for name too long checks, race conditions
> +         on local name buffer, and for socket close race condition.
> +         Thanks to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Fix that validator caps number of ANY RRsets it can
> +         validate, and the wait timer is shortened. Thanks to Qifan
> +         Zhang, Palo Alto Networks, for the report.
> +       - Fix analyzer warning in mesh_new_client.
> +
> +26 May 2026: Wouter
> +       - Fix for mesh new client and mesh new callback to rollback the
> +         added address, tcp mesh state and callback when there is a
> failure
> +         to initialize. This fixes the mesh accounting of reply addresses.
> +         Thanks to Xin Wang, Jiapeng Li, and Jiajia Liu, Northwestern
> +         Polytechnical University, for the report
> +
>  20 May 2026: Wouter
>         - Fix CVE-2026-33278, Possible remote code execution during DNSSEC
>           validation. Thanks to Qifan Zhang, Palo Alto Networks, for the
> report.
> @@ -27,6 +531,79 @@
>           Networks, for the report.
>         - Fix CVE-2026-44608, Use after free and crash in RPZ code. Thanks
>           to Qifan Zhang, Palo Alto Networks, for the report.
> +       - Tag for 1.25.1 release, it contains the security fixes on 1.25.0.
> +         the code repository continues with in addition the previous
> fixes,
> +         for 1.25.2.
> +       - Unit test for CVE-2026-33278.
> +       - Unit test for CVE-2026-42944.
> +       - Unit test for CVE-2026-42959.
> +       - Unit test for CVE-2026-40622.
> +       - Unit test for CVE-2026-42960.
> +       - Fix in depth for serve-expired responses from cachedb, that it
> +         does not store bogus. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix lame server detection, for selfpointed glue records.
> +         Thanks to Shuhan Zhang, Dan Li, and Baojun Liu from Tsinghua
> +         University for the report.
> +       - Fix cleaning up DoH session. The same query can be on multiple
> +         streams in a session. Thanks to Qifan Zhang, Palo Alto Networks,
> +         for the report.
> +       - Fix for signed same-owner CNAME and ordinary RRset responses.
> +         Thanks to Xin Wang and Jiajia Liu, Northwestern Polytechnical
> +         University, for the report.
> +
> +18 May 2026: Wouter
> +       - Fix for mixed class referrals, the resolver uses the query
> +         class. Thanks to Xin Wang and Jiajia Liu, Northwestern
> +         Polytechnical University, for the report.
> +
> +15 May 2026: Wouter
> +       - Fix man page entry for so-sndbuf, it is for responses sent out.
> +       - Fix val_find_DS for robustness, to check the result of
> +         packet_rrset_copy_region before using it. Thanks to Xin Wang
> +         and Jiajia Liu, Northwestern Polytechnical University, for
> +         the report.
> +       - Fix that for dns64 answers, the AAAA query is checked to be
> +         DNSSEC validated, when DNSSEC is enabled. This improves
> +         the RFC6147 conformance of Unbound. Thanks to Xin Wang
> +         and Jiajia Liu, Northwestern Polytechnical University, for
> +         the report. In addition, thanks to Qifan Zhang, Palo Alto
> +         Networks, for reporting it.
> +       - Fix for allocation-failure hardening of rrset cache wildcard
> +         storage and canonical NSEC owner replacement. Thanks to Xin
> +         Wang and Jiajia Liu, Northwestern Polytechnical University,
> +         for the report.
> +       - Fix DNSSEC validation with libnettle for noncanonical RSA
> +         DNSKEYs with leading zeroes for n. Thanks to Xin Wang and
> +         Jiajia Liu, Northwestern Polytechnical University, for
> +         the report.
> +       - Fix DNSKEY size calculation for noncanonical RSA DNSKEYs
> +         with leading zeroes for n. Thanks to Xin Wang and Jiajia Liu,
> +         Northwestern Polytechnical University, for the report.
> +
> +11 May 2026: Yorgos
> +       - Fix comment and verbose logging for EDNS fallback buffer size.
> +
> +8 May 2026: Wouter
> +       - Fix to relax assertions after the TTL 0 handling change.
> +         This relaxes an assertion in cachedb (it fails instead),
> +         and for packet_rrset_copy_region.
> +
> +7 May 2026: Wouter
> +       - Fix for Heap Out-of-Bounds Write via size_t-to-int Truncation
> +         in setup_if() - outside_network_create(). This fixes that
> +         large values for num_ports do not overflow and create
> +         invalid references after integer truncation. Thanks
> +         to Karnakar Reddy (@karnakarreddi) for the report.
> +       - Fix to clean up log ids after a failure to start a worker thread.
> +
> +1 May 2026: Wouter
> +       - iana portlist updated.
> +
> +29 April 2026: Wouter
> +       - tag for 1.25.0. The code repository continues with 1.25.1 in
> +         development.
> +       - Fix windows 64bit build for libssp dependency.
>
>  23 April 2026: Wouter
>         - Merge #1441: Fix buffer overrun in
> Index: README.md
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/README.md,v
> diff -u -p -r1.10 README.md
> --- README.md   26 May 2026 11:14:10 -0000      1.10
> +++ README.md   20 Sep 2026 09:50:47 -0000
> @@ -10,7 +10,7 @@ Unbound is a validating, recursive, cach
>  fast and lean and incorporates modern features based on open standards.
> If you
>  have any feedback, we would love to hear from you. Don’t hesitate to
>  [create an issue on Github](
> https://github.com/NLnetLabs/unbound/issues/new)
> -or post a message on the [Unbound mailing list](
> https://lists.nlnetlabs.nl/mailman/listinfo/unbound-users).
> +or post a message on our [community forum](
> https://community.nlnetlabs.nl/).
>  You can learn more about Unbound by reading our
>  [documentation](https://unbound.docs.nlnetlabs.nl/).
>
> Index: ax_pthread.m4
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/ax_pthread.m4,v
> diff -u -p -r1.3 ax_pthread.m4
> --- ax_pthread.m4       13 Jun 2024 14:30:28 -0000      1.3
> +++ ax_pthread.m4       20 Sep 2026 09:50:47 -0000
> @@ -87,7 +87,7 @@
>  #   modified version of the Autoconf Macro, you may extend this special
>  #   exception to the GPL to apply to your modified version as well.
>
> -#serial 31
> +#serial 32
>
>  AU_ALIAS([ACX_PTHREAD], [AX_PTHREAD])
>  AC_DEFUN([AX_PTHREAD], [
> @@ -249,7 +249,22 @@ AS_IF([test "x$ax_pthread_clang" = "xyes
>  # correctly enabled
>
>  case $host_os in
> -        darwin* | hpux* | linux* | osf* | solaris*)
> +        solaris*)
> +        # Solaris 11.4 introduced XPG7 support and did away with the need
> for
> +        # _REENTRANT.
> +
> +        AC_EGREP_CPP([AX_PTHREAD_SOLARIS__REENTRANT],
> +            [
> +#            undef _XOPEN_SOURCE
> +#            include <sys/feature_tests.h>
> +#            if _XOPEN_VERSION < 700
> +             AX_PTHREAD_SOLARIS__REENTRANT
> +#            endif
> +            ],
> +            [ax_pthread_check_macro="_REENTRANT"],
> +            [ax_pthread_check_macro="--"])
> +        ;;
> +        darwin* | hpux* | linux* | osf*)
>          ax_pthread_check_macro="_REENTRANT"
>          ;;
>
> Index: config.h.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/config.h.in,v
> diff -u -p -r1.37 config.h.in
> --- config.h.in 26 May 2026 11:14:10 -0000      1.37
> +++ config.h.in 20 Sep 2026 09:50:47 -0000
> @@ -31,6 +31,9 @@
>  /* Whether daemon is deprecated */
>  #undef DEPRECATED_DAEMON
>
> +/* Whether X509_NAME_get_text_by_NID is deprecated */
> +#undef DEPRECATED_X509_NAME_GET_TEXT_BY_NID
> +
>  /* Deprecate RSA 1024 bit length, makes that an unsupported key */
>  #undef DEPRECATE_RSA_1024
>
> @@ -60,6 +63,9 @@
>  /* Define to 1 if you have the <arpa/inet.h> header file. */
>  #undef HAVE_ARPA_INET_H
>
> +/* Define to 1 if you have the `ASN1_STRING_get0_data' function. */
> +#undef HAVE_ASN1_STRING_GET0_DATA
> +
>  /* Whether the C compiler accepts the "fallthrough" attribute */
>  #undef HAVE_ATTR_FALLTHROUGH
>
> @@ -140,6 +146,10 @@
>     to 0 if you don't. */
>  #undef HAVE_DECL_NGTCP2_CRYPTO_ENCRYPT_CB
>
> +/* Define to 1 if you have the declaration of
> `ngtcp2_crypto_ossl_ctx_new',
> +   and to 0 if you don't. */
> +#undef HAVE_DECL_NGTCP2_CRYPTO_OSSL_CTX_NEW
> +
>  /* Define to 1 if you have the declaration of `NID_ED25519', and to 0 if
> you
>     don't. */
>  #undef HAVE_DECL_NID_ED25519
> @@ -289,6 +299,12 @@
>  /* Define to 1 if you have the `FIPS_mode' function. */
>  #undef HAVE_FIPS_MODE
>
> +/* Define to 1 if you have the `fnmatch' function. */
> +#undef HAVE_FNMATCH
> +
> +/* Define to 1 if you have the <fnmatch.h> header file. */
> +#undef HAVE_FNMATCH_H
> +
>  /* Define to 1 if you have the `fork' function. */
>  #undef HAVE_FORK
>
> @@ -513,6 +529,9 @@
>  /* Define to 1 if you have the <openssl/bn.h> header file. */
>  #undef HAVE_OPENSSL_BN_H
>
> +/* Define to 1 if you have the `OPENSSL_cleanup' function. */
> +#undef HAVE_OPENSSL_CLEANUP
> +
>  /* Define to 1 if you have the `OPENSSL_config' function. */
>  #undef HAVE_OPENSSL_CONFIG
>
> @@ -685,9 +704,16 @@
>  /* Define to 1 if you have the `SSL_is_quic' function. */
>  #undef HAVE_SSL_IS_QUIC
>
> +/* Define to 1 if you have the `SSL_set1_dnsname' function. */
> +#undef HAVE_SSL_SET1_DNSNAME
> +
>  /* Define to 1 if you have the `SSL_set1_host' function. */
>  #undef HAVE_SSL_SET1_HOST
>
> +/* Define to 1 if you have the `SSL_set_quic_tls_early_data_enabled'
> function.
> +   */
> +#undef HAVE_SSL_SET_QUIC_TLS_EARLY_DATA_ENABLED
> +
>  /* Define to 1 if you have the <stdarg.h> header file. */
>  #undef HAVE_STDARG_H
>
> @@ -851,6 +877,12 @@
>
>  /* Define to 1 if you have the <ws2tcpip.h> header file. */
>  #undef HAVE_WS2TCPIP_H
> +
> +/* Define to 1 if you have the `X509_get_key_usage' function. */
> +#undef HAVE_X509_GET_KEY_USAGE
> +
> +/* Define to 1 if you have the `X509_NAME_get_text_by_NID' function. */
> +#undef HAVE_X509_NAME_GET_TEXT_BY_NID
>
>  /* Define to 1 if you have the `X509_VERIFY_PARAM_set1_host' function. */
>  #undef HAVE_X509_VERIFY_PARAM_SET1_HOST
> Index: configure
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/configure,v
> diff -u -p -r1.63 configure
> --- configure   27 Jul 2026 14:14:39 -0000      1.63
> +++ configure   20 Sep 2026 09:50:47 -0000
> @@ -1,6 +1,6 @@
>  #! /bin/sh
>  # Guess values for system-dependent variables and create Makefiles.
> -# Generated by GNU Autoconf 2.71 for unbound 1.25.2.
> +# Generated by GNU Autoconf 2.71 for unbound 1.26.1.
>  #
>  # Report bugs to <unbound-bugs@nlnetlabs.nl or
> https://github.com/NLnetLabs/unbound/issues>.
>  #
> @@ -622,8 +622,8 @@ MAKEFLAGS=
>  # Identity of this package.
>  PACKAGE_NAME='unbound'
>  PACKAGE_TARNAME='unbound'
> -PACKAGE_VERSION='1.25.2'
> -PACKAGE_STRING='unbound 1.25.2'
> +PACKAGE_VERSION='1.26.1'
> +PACKAGE_STRING='unbound 1.26.1'
>  PACKAGE_BUGREPORT='unbound-bugs@nlnetlabs.nl or
> https://github.com/NLnetLabs/unbound/issues'
>  PACKAGE_URL=''
>
> @@ -1516,7 +1516,7 @@ if test "$ac_init_help" = "long"; then
>    # Omit some internal or obsolete options to make the list less imposing.
>    # This message is too long to be a string in the A/UX 3.1 sh.
>    cat <<_ACEOF
> -\`configure' configures unbound 1.25.2 to adapt to many kinds of systems.
> +\`configure' configures unbound 1.26.1 to adapt to many kinds of systems.
>
>  Usage: $0 [OPTION]... [VAR=VALUE]...
>
> @@ -1582,7 +1582,7 @@ fi
>
>  if test -n "$ac_init_help"; then
>    case $ac_init_help in
> -     short | recursive ) echo "Configuration of unbound 1.25.2:";;
> +     short | recursive ) echo "Configuration of unbound 1.26.1:";;
>     esac
>    cat <<\_ACEOF
>
> @@ -1835,7 +1835,7 @@ fi
>  test -n "$ac_init_help" && exit $ac_status
>  if $ac_init_version; then
>    cat <<\_ACEOF
> -unbound configure 1.25.2
> +unbound configure 1.26.1
>  generated by GNU Autoconf 2.71
>
>  Copyright (C) 2021 Free Software Foundation, Inc.
> @@ -2492,7 +2492,7 @@ cat >config.log <<_ACEOF
>  This file contains any messages produced by compilers while
>  running configure, to aid debugging if configure makes a mistake.
>
> -It was created by unbound $as_me 1.25.2, which was
> +It was created by unbound $as_me 1.26.1, which was
>  generated by GNU Autoconf 2.71.  Invocation command line was
>
>    $ $0$ac_configure_args_raw
> @@ -3254,13 +3254,13 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu
>
>  UNBOUND_VERSION_MAJOR=1
>
> -UNBOUND_VERSION_MINOR=25
> +UNBOUND_VERSION_MINOR=26
>
> -UNBOUND_VERSION_MICRO=2
> +UNBOUND_VERSION_MICRO=1
>
>
>  LIBUNBOUND_CURRENT=9
> -LIBUNBOUND_REVISION=38
> +LIBUNBOUND_REVISION=40
>  LIBUNBOUND_AGE=1
>  # 1.0.0 had 0:12:0
>  # 1.0.1 had 0:13:0
> @@ -3366,6 +3366,8 @@ LIBUNBOUND_AGE=1
>  # 1.25.0 had 9:36:1
>  # 1.25.1 had 9:37:1
>  # 1.25.2 had 9:38:1
> +# 1.26.0 had 9:39:1
> +# 1.26.1 had 9:40:1
>
>  #   Current  -- the number of the binary API that we're implementing
>  #   Revision -- which iteration of the implementation of the binary
> @@ -16761,6 +16763,13 @@ then :
>    printf "%s\n" "#define HAVE_GLOB_H 1" >>confdefs.h
>
>  fi
> +ac_fn_c_check_header_compile "$LINENO" "fnmatch.h"
> "ac_cv_header_fnmatch_h" "$ac_includes_default
> +"
> +if test "x$ac_cv_header_fnmatch_h" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_FNMATCH_H 1" >>confdefs.h
> +
> +fi
>  ac_fn_c_check_header_compile "$LINENO" "grp.h" "ac_cv_header_grp_h"
> "$ac_includes_default
>  "
>  if test "x$ac_cv_header_grp_h" = xyes
> @@ -19162,7 +19171,31 @@ fi
>  # correctly enabled
>
>  case $host_os in
> -        darwin* | hpux* | linux* | osf* | solaris*)
> +        solaris*)
> +        # Solaris 11.4 introduced XPG7 support and did away with the need
> for
> +        # _REENTRANT.
> +
> +        cat confdefs.h - <<_ACEOF >conftest.$ac_ext
> +/* end confdefs.h.  */
> +
> +#            undef _XOPEN_SOURCE
> +#            include <sys/feature_tests.h>
> +#            if _XOPEN_VERSION < 700
> +             AX_PTHREAD_SOLARIS__REENTRANT
> +#            endif
> +
> +_ACEOF
> +if (eval "$ac_cpp conftest.$ac_ext") 2>&5 |
> +  $EGREP "AX_PTHREAD_SOLARIS__REENTRANT" >/dev/null 2>&1
> +then :
> +  ax_pthread_check_macro="_REENTRANT"
> +else $as_nop
> +  ax_pthread_check_macro="--"
> +fi
> +rm -rf conftest*
> +
> +        ;;
> +        darwin* | hpux* | linux* | osf*)
>          ax_pthread_check_macro="_REENTRANT"
>          ;;
>
> @@ -21816,6 +21849,12 @@ then :
>    printf "%s\n" "#define HAVE_BIO_SET_CALLBACK_EX 1" >>confdefs.h
>
>  fi
> +ac_fn_c_check_func "$LINENO" "OPENSSL_cleanup"
> "ac_cv_func_OPENSSL_cleanup"
> +if test "x$ac_cv_func_OPENSSL_cleanup" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_OPENSSL_CLEANUP 1" >>confdefs.h
> +
> +fi
>
>
>  # these check_funcs need -lssl
> @@ -21845,6 +21884,24 @@ then :
>    printf "%s\n" "#define HAVE_SSL_GET0_PEERNAME 1" >>confdefs.h
>
>  fi
> +ac_fn_c_check_func "$LINENO" "SSL_set1_dnsname"
> "ac_cv_func_SSL_set1_dnsname"
> +if test "x$ac_cv_func_SSL_set1_dnsname" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_SSL_SET1_DNSNAME 1" >>confdefs.h
> +
> +fi
> +ac_fn_c_check_func "$LINENO" "X509_get_key_usage"
> "ac_cv_func_X509_get_key_usage"
> +if test "x$ac_cv_func_X509_get_key_usage" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_X509_GET_KEY_USAGE 1" >>confdefs.h
> +
> +fi
> +ac_fn_c_check_func "$LINENO" "ASN1_STRING_get0_data"
> "ac_cv_func_ASN1_STRING_get0_data"
> +if test "x$ac_cv_func_ASN1_STRING_get0_data" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_ASN1_STRING_GET0_DATA 1" >>confdefs.h
> +
> +fi
>  ac_fn_c_check_func "$LINENO" "X509_VERIFY_PARAM_set1_host"
> "ac_cv_func_X509_VERIFY_PARAM_set1_host"
>  if test "x$ac_cv_func_X509_VERIFY_PARAM_set1_host" = xyes
>  then :
> @@ -21888,6 +21945,54 @@ then :
>
>  fi
>
> +ac_fn_c_check_func "$LINENO" "X509_NAME_get_text_by_NID"
> "ac_cv_func_X509_NAME_get_text_by_NID"
> +if test "x$ac_cv_func_X509_NAME_get_text_by_NID" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_X509_NAME_GET_TEXT_BY_NID 1" >>confdefs.h
> +
> +fi
> +
> +if test $ac_cv_func_X509_NAME_get_text_by_NID = yes; then
> +
> +
> +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking if
> X509_NAME_get_text_by_NID is deprecated" >&5
> +printf %s "checking if X509_NAME_get_text_by_NID is deprecated... " >&6; }
> +cache=`echo X509_NAME_get_text_by_NID | sed 'y%.=/+-%___p_%'`
> +if eval test \${cv_cc_deprecated_$cache+y}
> +then :
> +  printf %s "(cached) " >&6
> +else $as_nop
> +
> +echo '
> +#include "openssl/x509.h"
> +' >conftest.c
> +echo 'void f(void){
> +               (void)X509_NAME_get_text_by_NID(NULL, 0, NULL, 0); }'
> >>conftest.c
> +if test -z "`$CC $CPPFLAGS $CFLAGS -c conftest.c 2>&1 | grep -e
> deprecated -e unavailable`"; then
> +eval "cv_cc_deprecated_$cache=no"
> +else
> +eval "cv_cc_deprecated_$cache=yes"
> +fi
> +rm -f conftest conftest.o conftest.c
> +
> +fi
> +
> +if eval "test \"`echo '$cv_cc_deprecated_'$cache`\" = yes"; then
> +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
> +printf "%s\n" "yes" >&6; }
> +
> +printf "%s\n" "#define DEPRECATED_X509_NAME_GET_TEXT_BY_NID 1"
> >>confdefs.h
> +
> +:
> +
> +else
> +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
> +printf "%s\n" "no" >&6; }
> +:
> +
> +fi
> +
> +fi
>  LIBS="$BAKLIBS"
>
>  ac_fn_check_decl "$LINENO" "SSL_COMP_get_compression_methods"
> "ac_cv_have_decl_SSL_COMP_get_compression_methods" "
> @@ -23394,6 +23499,24 @@ then :
>
>  printf "%s\n" "#define USE_NGTCP2_CRYPTO_OSSL 1" >>confdefs.h
>
> +           ac_fn_check_decl "$LINENO" "ngtcp2_crypto_ossl_ctx_new"
> "ac_cv_have_decl_ngtcp2_crypto_ossl_ctx_new" "$ac_includes_default
> +           #include <ngtcp2/ngtcp2_crypto_ossl.h>
> +
> +" "$ac_c_undeclared_builtin_options" "CFLAGS"
> +if test "x$ac_cv_have_decl_ngtcp2_crypto_ossl_ctx_new" = xyes
> +then :
> +  ac_have_decl=1
> +else $as_nop
> +  ac_have_decl=0
> +fi
> +printf "%s\n" "#define HAVE_DECL_NGTCP2_CRYPTO_OSSL_CTX_NEW
> $ac_have_decl" >>confdefs.h
> +if test $ac_have_decl = 1
> +then :
> +
> +else $as_nop
> +  as_fn_error $? "No declaration of ngtcp2_crypto_ossl_ctx_new in the
> ngtcp2_crypto_ossl header file. Perhaps the ngtcp2_crypto_ossl devel header
> files need to be installed." "$LINENO" 5
> +fi
> +
>
>  else $as_nop
>
> @@ -23573,6 +23696,13 @@ else $as_nop
>  fi
>
>  done
> +    ac_fn_c_check_func "$LINENO" "SSL_set_quic_tls_early_data_enabled"
> "ac_cv_func_SSL_set_quic_tls_early_data_enabled"
> +if test "x$ac_cv_func_SSL_set_quic_tls_early_data_enabled" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_SSL_SET_QUIC_TLS_EARLY_DATA_ENABLED 1"
> >>confdefs.h
> +
> +fi
> +
>      LIBS="$BAKLIBS"
>
>      ac_fn_c_check_type "$LINENO" "struct ngtcp2_version_cid"
> "ac_cv_type_struct_ngtcp2_version_cid" "$ac_includes_default
> @@ -24536,6 +24666,12 @@ then :
>    printf "%s\n" "#define HAVE_GLOB 1" >>confdefs.h
>
>  fi
> +ac_fn_c_check_func "$LINENO" "fnmatch" "ac_cv_func_fnmatch"
> +if test "x$ac_cv_func_fnmatch" = xyes
> +then :
> +  printf "%s\n" "#define HAVE_FNMATCH 1" >>confdefs.h
> +
> +fi
>  ac_fn_c_check_func "$LINENO" "initgroups" "ac_cv_func_initgroups"
>  if test "x$ac_cv_func_initgroups" = xyes
>  then :
> @@ -26331,7 +26467,7 @@ printf "%s\n" "#define MAXSYSLOGMSGLEN 1
>
>
>
> -version=1.25.2
> +version=1.26.1
>
>  { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for build time" >&5
>  printf %s "checking for build time... " >&6; }
> @@ -26861,7 +26997,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_wri
>  # report actual input values of CONFIG_FILES etc. instead of their
>  # values after options handling.
>  ac_log="
> -This file was extended by unbound $as_me 1.25.2, which was
> +This file was extended by unbound $as_me 1.26.1, which was
>  generated by GNU Autoconf 2.71.  Invocation command line was
>
>    CONFIG_FILES    = $CONFIG_FILES
> @@ -26929,7 +27065,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac
>  cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
>  ac_cs_config='$ac_cs_config_escaped'
>  ac_cs_version="\\
> -unbound config.status 1.25.2
> +unbound config.status 1.26.1
>  configured by $0, generated by GNU Autoconf 2.71,
>    with options \\"\$ac_cs_config\\"
>
> Index: configure.ac
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/configure.ac,v
> diff -u -p -r1.63 configure.ac
> --- configure.ac        27 Jul 2026 14:14:39 -0000      1.63
> +++ configure.ac        20 Sep 2026 09:50:47 -0000
> @@ -11,15 +11,15 @@ sinclude(dnscrypt/dnscrypt.m4)
>
>  # must be numbers. ac_defun because of later processing
>  m4_define([VERSION_MAJOR],[1])
> -m4_define([VERSION_MINOR],[25])
> -m4_define([VERSION_MICRO],[2])
> +m4_define([VERSION_MINOR],[26])
> +m4_define([VERSION_MICRO],[1])
>
>  AC_INIT([unbound],m4_defn([VERSION_MAJOR]).m4_defn([VERSION_MINOR]).m4_defn([VERSION_MICRO]),[
> unbound-bugs@nlnetlabs.nl or https://github.com/NLnetLabs/unbound/issues
> ],[unbound])
>  AC_SUBST(UNBOUND_VERSION_MAJOR, [VERSION_MAJOR])
>  AC_SUBST(UNBOUND_VERSION_MINOR, [VERSION_MINOR])
>  AC_SUBST(UNBOUND_VERSION_MICRO, [VERSION_MICRO])
>
>  LIBUNBOUND_CURRENT=9
> -LIBUNBOUND_REVISION=38
> +LIBUNBOUND_REVISION=40
>  LIBUNBOUND_AGE=1
>  # 1.0.0 had 0:12:0
>  # 1.0.1 had 0:13:0
> @@ -125,6 +125,8 @@ LIBUNBOUND_AGE=1
>  # 1.25.0 had 9:36:1
>  # 1.25.1 had 9:37:1
>  # 1.25.2 had 9:38:1
> +# 1.26.0 had 9:39:1
> +# 1.26.1 had 9:40:1
>
>  #   Current  -- the number of the binary API that we're implementing
>  #   Revision -- which iteration of the implementation of the binary
> @@ -483,7 +485,7 @@ PKG_PROG_PKG_CONFIG
>  fi
>
>  # Checks for header files.
> -AC_CHECK_HEADERS([stdarg.h stdbool.h netinet/in.h netinet/tcp.h
> sys/param.h sys/select.h sys/socket.h sys/un.h sys/uio.h sys/resource.h
> arpa/inet.h syslog.h netdb.h sys/wait.h pwd.h glob.h grp.h login_cap.h
> winsock2.h ws2tcpip.h endian.h sys/endian.h libkern/OSByteOrder.h sys/ipc.h
> sys/shm.h ifaddrs.h poll.h],,, [AC_INCLUDES_DEFAULT])
> +AC_CHECK_HEADERS([stdarg.h stdbool.h netinet/in.h netinet/tcp.h
> sys/param.h sys/select.h sys/socket.h sys/un.h sys/uio.h sys/resource.h
> arpa/inet.h syslog.h netdb.h sys/wait.h pwd.h glob.h fnmatch.h grp.h
> login_cap.h winsock2.h ws2tcpip.h endian.h sys/endian.h
> libkern/OSByteOrder.h sys/ipc.h sys/shm.h ifaddrs.h poll.h],,,
> [AC_INCLUDES_DEFAULT])
>  # net/if.h portability for Darwin see:
>  #
> https://www.gnu.org/software/autoconf/manual/autoconf-2.69/html_node/Header-Portability.html
>  AC_CHECK_HEADERS([net/if.h],,, [
> @@ -1080,12 +1082,19 @@ else
>         AC_MSG_RESULT([no])
>  fi
>  AC_CHECK_HEADERS([openssl/conf.h openssl/engine.h openssl/bn.h
> openssl/dh.h openssl/dsa.h openssl/rsa.h openssl/core_names.h
> openssl/param_build.h],,, [AC_INCLUDES_DEFAULT])
> -AC_CHECK_FUNCS([OPENSSL_config EVP_sha1 EVP_sha256 EVP_sha512 FIPS_mode
> EVP_default_properties_is_fips_enabled EVP_MD_CTX_new
> OpenSSL_add_all_digests OPENSSL_init_crypto EVP_cleanup ENGINE_cleanup
> ERR_load_crypto_strings CRYPTO_cleanup_all_ex_data ERR_free_strings
> RAND_cleanup DSA_SIG_set0 EVP_dss1 EVP_DigestVerify EVP_aes_256_cbc
> EVP_EncryptInit_ex HMAC_Init_ex CRYPTO_THREADID_set_callback
> EVP_MAC_CTX_set_params OSSL_PARAM_BLD_new BIO_set_callback_ex])
> +AC_CHECK_FUNCS([OPENSSL_config EVP_sha1 EVP_sha256 EVP_sha512 FIPS_mode
> EVP_default_properties_is_fips_enabled EVP_MD_CTX_new
> OpenSSL_add_all_digests OPENSSL_init_crypto EVP_cleanup ENGINE_cleanup
> ERR_load_crypto_strings CRYPTO_cleanup_all_ex_data ERR_free_strings
> RAND_cleanup DSA_SIG_set0 EVP_dss1 EVP_DigestVerify EVP_aes_256_cbc
> EVP_EncryptInit_ex HMAC_Init_ex CRYPTO_THREADID_set_callback
> EVP_MAC_CTX_set_params OSSL_PARAM_BLD_new BIO_set_callback_ex
> OPENSSL_cleanup])
>
>  # these check_funcs need -lssl
>  BAKLIBS="$LIBS"
>  LIBS="-lssl $LIBS"
> -AC_CHECK_FUNCS([OPENSSL_init_ssl SSL_CTX_set_security_level SSL_set1_host
> SSL_get0_peername X509_VERIFY_PARAM_set1_host SSL_CTX_set_ciphersuites
> SSL_CTX_set_tlsext_ticket_key_evp_cb SSL_CTX_set_alpn_select_cb
> SSL_get0_alpn_selected SSL_CTX_set_alpn_protos SSL_get1_peer_certificate])
> +AC_CHECK_FUNCS([OPENSSL_init_ssl SSL_CTX_set_security_level SSL_set1_host
> SSL_get0_peername SSL_set1_dnsname X509_get_key_usage ASN1_STRING_get0_data
> X509_VERIFY_PARAM_set1_host SSL_CTX_set_ciphersuites
> SSL_CTX_set_tlsext_ticket_key_evp_cb SSL_CTX_set_alpn_select_cb
> SSL_get0_alpn_selected SSL_CTX_set_alpn_protos SSL_get1_peer_certificate])
> +AC_CHECK_FUNCS([X509_NAME_get_text_by_NID])
> +if test $ac_cv_func_X509_NAME_get_text_by_NID = yes; then
> +       ACX_FUNC_DEPRECATED([X509_NAME_get_text_by_NID], [
> +               (void)X509_NAME_get_text_by_NID(NULL, 0, NULL, 0);], [
> +#include "openssl/x509.h"
> +])
> +fi
>  LIBS="$BAKLIBS"
>
>  AC_CHECK_DECLS([SSL_COMP_get_compression_methods,sk_SSL_COMP_pop_free,SSL_CTX_set_ecdh_auto,SSL_CTX_set_tmp_ecdh],
> [], [], [
> @@ -1704,6 +1713,9 @@ if test x_$withval = x_yes -o x_$withval
>      AC_CHECK_LIB([ngtcp2_crypto_ossl], [ngtcp2_crypto_encrypt_cb], [
>         LIBS="$LIBS -lngtcp2_crypto_ossl"
>         AC_DEFINE(USE_NGTCP2_CRYPTO_OSSL, 1, [Define this to use
> ngtcp2_crypto_ossl.])
> +           AC_CHECK_DECLS([ngtcp2_crypto_ossl_ctx_new], [],
> [AC_MSG_ERROR([No declaration of ngtcp2_crypto_ossl_ctx_new in the
> ngtcp2_crypto_ossl header file. Perhaps the ngtcp2_crypto_ossl devel header
> files need to be installed.])], [AC_INCLUDES_DEFAULT
> +           #include <ngtcp2/ngtcp2_crypto_ossl.h>
> +           ])
>      ], [
>          AC_CHECK_LIB([ngtcp2_crypto_openssl], [ngtcp2_crypto_encrypt_cb],
> [ LIBS="$LIBS -lngtcp2_crypto_openssl" ], [
>              AC_CHECK_LIB([ngtcp2_crypto_quictls],
> [ngtcp2_crypto_encrypt_cb], [ LIBS="$LIBS -lngtcp2_crypto_quictls" ])
> @@ -1715,6 +1727,7 @@ if test x_$withval = x_yes -o x_$withval
>      BAKLIBS="$LIBS"
>      LIBS="-lssl $LIBS"
>      AC_CHECK_FUNCS([SSL_is_quic], [], [AC_MSG_ERROR([No QUIC support
> detected in OpenSSL. Need OpenSSL version with QUIC support to enable DNS
> over QUIC with libngtcp2.])])
> +    AC_CHECK_FUNCS([SSL_set_quic_tls_early_data_enabled])
>      LIBS="$BAKLIBS"
>
>      AC_CHECK_TYPES([struct ngtcp2_version_cid,
> ngtcp2_encryption_level],,,[AC_INCLUDES_DEFAULT
> @@ -1928,7 +1941,7 @@ AC_LINK_IFELSE([AC_LANG_PROGRAM([
>    AC_MSG_RESULT(no))
>
>  AC_SEARCH_LIBS([setusercontext], [util])
> -AC_CHECK_FUNCS([tzset sigprocmask fcntl getpwnam endpwent getrlimit
> setrlimit setsid chroot kill chown sleep usleep random srandom recvmsg
> sendmsg writev socketpair glob initgroups strftime localtime_r
> setusercontext _beginthreadex endservent endprotoent fsync shmget accept4
> getifaddrs if_nametoindex poll gettid])
> +AC_CHECK_FUNCS([tzset sigprocmask fcntl getpwnam endpwent getrlimit
> setrlimit setsid chroot kill chown sleep usleep random srandom recvmsg
> sendmsg writev socketpair glob fnmatch initgroups strftime localtime_r
> setusercontext _beginthreadex endservent endprotoent fsync shmget accept4
> getifaddrs if_nametoindex poll gettid])
>  AC_CHECK_FUNCS([setresuid],,[AC_CHECK_FUNCS([setreuid])])
>  AC_CHECK_FUNCS([setresgid],,[AC_CHECK_FUNCS([setregid])])
>
> Index: cachedb/cachedb.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/cachedb/cachedb.c,v
> diff -u -p -r1.23 cachedb.c
> --- cachedb/cachedb.c   26 May 2026 11:14:11 -0000      1.23
> +++ cachedb/cachedb.c   20 Sep 2026 09:50:47 -0000
> @@ -401,6 +401,12 @@ prep_data(struct module_qstate* qstate,
>            FLAGS_GET_RCODE(qstate->return_msg->rep->flags) !=
>                 LDNS_RCODE_YXDOMAIN)
>                 return 0;
> +       /* Do not persist data the validator has not yet seen, or has
> rejected.
> +        * Otherwise an expired blob could maybe reach clients via
> +        * serve-expired. */
> +       if(qstate->env->need_to_validate &&
> +               qstate->return_msg->rep->security == sec_status_bogus)
> +               return 0;
>         /* We don't store the reply if its TTL is 0. This is probably
> coming
>          * from upstream and it is not meant to be stored. */
>         if(qstate->return_msg->rep->ttl == 0)
> @@ -863,6 +869,11 @@ cachedb_handle_query(struct module_qstat
>                                 return;
>                 }
>                 /* No 0TTL answers escaping from external cache. */
> +               if(qstate->return_msg->rep->ttl == 0) {
> +                       qstate->return_msg = NULL;
> +                       qstate->ext_state[id] = module_wait_module;
> +                       return;
> +               }
>                 log_assert(qstate->return_msg->rep->ttl > 0);
>                 qstate->is_cachedb_answer = 1;
>                 /* we are done with the query */
> Index: daemon/cachedump.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/cachedump.c,v
> diff -u -p -r1.13 cachedump.c
> --- daemon/cachedump.c  26 Sep 2025 07:32:37 -0000      1.13
> +++ daemon/cachedump.c  20 Sep 2026 09:50:47 -0000
> @@ -99,7 +99,7 @@ static void
>  dump_rrset_line(struct config_strlist_head* txt, struct
> ub_packed_rrset_key* k,
>         time_t now, size_t i)
>  {
> -       char s[65535];
> +       char s[65535*4+2048];
>         if(!packed_rr_to_string(k, i, now, s, sizeof(s))) {
>                 spool_txt_string(txt, "BADRR\n");
>                 return;
> @@ -455,7 +455,7 @@ load_rr(RES* ssl, sldns_buffer* buf, str
>         /* read the line */
>         if(!ssl_read_buf(ssl, buf))
>                 return 0;
> -       if(strncmp((char*)sldns_buffer_begin(buf), "BADRR\n", 6) == 0) {
> +       if(strcmp((char*)sldns_buffer_begin(buf), "BADRR") == 0) {
>                 *go_on = 0;
>                 return 1;
>         }
> Index: daemon/daemon.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/daemon.c,v
> diff -u -p -r1.30 daemon.c
> --- daemon/daemon.c     27 Jul 2026 14:14:39 -0000      1.30
> +++ daemon/daemon.c     20 Sep 2026 09:50:47 -0000
> @@ -217,7 +217,8 @@ setup_listen_sslctx(void** ctx, int is_d
>                 (cfg->tls_session_ticket_keys.first &&
>                 cfg->tls_session_ticket_keys.first->str[0] != 0),
>                 is_dot, is_doh, cfg->tls_protocols))) {
> -               fatal_exit("could not set up listen SSL_CTX");
> +               log_err("could not set up listen SSL_CTX");
> +               *ctx = NULL;
>         }
>  }
>  #endif /* HAVE_SSL */
> @@ -259,7 +260,8 @@ void* daemon_setup_listen_quic_sslctx(st
>                 pem += strlen(chroot);
>
>         if(!(ctx = quic_sslctx_create(key, pem, NULL))) {
> -               fatal_exit("could not set up quic SSL_CTX");
> +               log_err("could not set up quic SSL_CTX");
> +               return NULL;
>         }
>         return ctx;
>  }
> @@ -277,8 +279,10 @@ void* daemon_setup_connect_dot_sslctx(st
>                 bundle += strlen(chroot);
>
>         if(!(ctx = connect_sslctx_create(NULL, NULL, bundle,
> -               cfg->tls_win_cert)))
> -               fatal_exit("could not set up connect SSL_CTX");
> +               cfg->tls_win_cert))) {
> +               log_err("could not set up connect SSL_CTX");
> +               return NULL;
> +       }
>         return ctx;
>  }
>  #endif /* HAVE_SSL */
> @@ -308,16 +312,22 @@ daemon_setup_sslctxs(struct daemon* daem
>                 }
>                 daemon->listen_dot_sslctx = daemon_setup_listen_dot_sslctx(
>                         daemon, cfg);
> +               if(!daemon->listen_dot_sslctx)
> +                       fatal_exit("Could not set up listen dot sslctx");
>  #ifdef HAVE_NGHTTP2_NGHTTP2_H
>                 if(cfg_has_https(cfg)) {
>                         daemon->listen_doh_sslctx =
>                                 daemon_setup_listen_doh_sslctx(daemon,
> cfg);
> +                       if(!daemon->listen_doh_sslctx)
> +                               fatal_exit("Could not set up listen doh
> sslctx");
>                 }
>  #endif
>  #ifdef HAVE_NGTCP2
>                 if(cfg_has_quic(cfg)) {
>                         daemon->listen_quic_sslctx =
>                                 daemon_setup_listen_quic_sslctx(daemon,
> cfg);
> +                       if(!daemon->listen_quic_sslctx)
> +                               fatal_exit("Could not set up listen quic
> sslctx");
>                 }
>  #endif /* HAVE_NGTCP2 */
>
> @@ -350,6 +360,8 @@ daemon_setup_sslctxs(struct daemon* daem
>         }
>         daemon->connect_dot_sslctx = daemon_setup_connect_dot_sslctx(
>                 daemon, cfg);
> +       if(!daemon->connect_dot_sslctx)
> +               fatal_exit("could not setup connect dot sslctx");
>  #else /* HAVE_SSL */
>         (void)daemon;(void)cfg;
>  #endif /* HAVE_SSL */
> @@ -601,7 +613,7 @@ int setup_acl_for_ports(struct acl_list*
>         return 1;
>  }
>
> -int
> +int
>  daemon_open_shared_ports(struct daemon* daemon)
>  {
>         log_assert(daemon);
> @@ -921,13 +933,14 @@ thread_start(void* arg)
>  {
>         struct worker* worker = (struct worker*)arg;
>         int port_num = 0;
> -       log_assert(worker->thr_id);
>         set_log_thread_id(worker, worker->daemon->cfg);
>         {
>                 char name[16]; /* seems to be the safest size between
>                                   different OSes */
>                 snprintf(name, sizeof(name), "unbound/%u",
> worker->thread_num);
> -               ub_thread_setname(worker->thr_id, name);
> +               /* worker->thr_id can be written to after the thread was
> made
> +                * by the creating thread, so this uses pthread_self. */
> +               ub_thread_setname(ub_thread_self(), name);
>         }
>         ub_thread_blocksigs();
>  #ifdef THREADS_DISABLED
> @@ -942,8 +955,9 @@ thread_start(void* arg)
>                 port_num = 0;
>  #endif
>         if(!worker_init(worker, worker->daemon->cfg,
> -                       worker->daemon->ports[port_num], 0))
> +                       worker->daemon->ports[port_num], 0)) {
>                 fatal_exit("Could not initialize thread");
> +       }
>
>         worker_work(worker);
>         return NULL;
> @@ -1105,8 +1119,9 @@ daemon_fork(struct daemon* daemon)
>
>  #if defined(HAVE_EV_LOOP) || defined(HAVE_EV_DEFAULT_LOOP)
>         /* in libev the first inited base gets signals */
> -       if(!worker_init(daemon->workers[0], daemon->cfg, daemon->ports[0],
> 1))
> +       if(!worker_init(daemon->workers[0], daemon->cfg, daemon->ports[0],
> 1)) {
>                 fatal_exit("Could not initialize main thread");
> +       }
>  #endif
>
>         /* Now create the threads and init the workers.
> @@ -1119,8 +1134,9 @@ daemon_fork(struct daemon* daemon)
>          */
>  #if !(defined(HAVE_EV_LOOP) || defined(HAVE_EV_DEFAULT_LOOP))
>         /* libevent has the last inited base get signals (or any base) */
> -       if(!worker_init(daemon->workers[0], daemon->cfg, daemon->ports[0],
> 1))
> +       if(!worker_init(daemon->workers[0], daemon->cfg, daemon->ports[0],
> 1)) {
>                 fatal_exit("Could not initialize main thread");
> +       }
>  #endif
>         signal_handling_playback(daemon->workers[0]);
>
> @@ -1164,7 +1180,6 @@ daemon_cleanup(struct daemon* daemon)
>         /* before stopping main worker, handle signals ourselves, so we
>            don't die on multiple reload signals for example. */
>         signal_handling_record();
> -       log_thread_set(NULL);
>         /* clean up caches because
>          * a) RRset IDs will be recycled after a reload, causing collisions
>          * b) validation config can change, thus rrset, msg, keycache clear
> @@ -1270,7 +1285,7 @@ daemon_delete(struct daemon* daemon)
>  #  if HAVE_DECL_SSL_COMP_GET_COMPRESSION_METHODS &&
> HAVE_DECL_SK_SSL_COMP_POP_FREE
>  #    ifndef S_SPLINT_S
>  #      if OPENSSL_VERSION_NUMBER < 0x10100000
> -       sk_SSL_COMP_pop_free(comp_meth, (void(*)())CRYPTO_free);
> +       sk_SSL_COMP_pop_free(comp_meth, (void(*)(SSL_COMP*))CRYPTO_free);
>  #      endif
>  #    endif
>  #  endif
> @@ -1293,6 +1308,9 @@ daemon_delete(struct daemon* daemon)
>  #  if defined(HAVE_SSL) && defined(OPENSSL_THREADS) &&
> !defined(THREADS_DISABLED)
>         ub_openssl_lock_delete();
>  #  endif
> +#ifdef HAVE_OPENSSL_CLEANUP
> +       OPENSSL_cleanup();
> +#endif
>  #ifndef HAVE_ARC4RANDOM
>         _ARC4_LOCK_DESTROY();
>  #endif
> Index: daemon/remote.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/remote.c,v
> diff -u -p -r1.46 remote.c
> --- daemon/remote.c     27 Jul 2026 14:14:39 -0000      1.46
> +++ daemon/remote.c     20 Sep 2026 09:50:47 -0000
> @@ -307,7 +307,7 @@ add_open(const char* ip, int nr, struct
>  #endif
>                 }
>         } else {
> -               char* s = strchr(ip, '@');
> +               const char* s = strchr(ip, '@');
>                 char newif[128];
>                 if(s) {
>                         /* override port with ifspec@port */
> @@ -1533,18 +1533,95 @@ do_datas_add(struct daemon_remote* rc, R
>         (void)ssl_printf(ssl, "added %d datas\n", num);
>  }
>
> +static int
> +perform_data_remove_rr(RES* ssl, struct local_zones* local_zones,
> +       uint8_t* rr, size_t len, size_t dname_len, char *arg)
> +{
> +       uint16_t rr_class, rr_type;
> +       int labs;
> +       struct local_zone* z;
> +       struct local_data* ld;
> +       uint8_t *rdata;
> +       size_t rdata_len, index;
> +       struct packed_rrset_data* d;
> +       struct local_rrset* p;
> +
> +       rdata = sldns_wirerr_get_rdatawl(rr, len, dname_len);
> +       rdata_len = ((size_t)sldns_wirerr_get_rdatalen(rr, len,
> dname_len))+2;
> +
> +       labs = dname_count_labels(rr);
> +
> +       rr_class = sldns_wirerr_get_class(rr, len, dname_len);
> +       rr_type = sldns_wirerr_get_type(rr, len, dname_len);
> +
> +       z = local_zones_lookup(local_zones, rr, dname_len,
> +                       labs, rr_class, rr_type, 1);
> +       if (!z) {
> +               ssl_printf(ssl, "error no zone for rr %s\n", arg);
> +               return 0;
> +       }
> +
> +       ld = local_zone_find_data(z, rr, dname_len, labs);
> +       if (!ld) {
> +               ssl_printf(ssl, "error no local data for rr %s\n", arg);
> +               return 0;
> +       }
> +
> +       p = ld->rrsets;
> +       while (p && ntohs(p->rrset->rk.type) != rr_type) {
> +               p = p->next;
> +       }
> +
> +       if (!p) {
> +               ssl_printf(ssl, "error no rrset for rr %s\n", arg);
> +               return 0;
> +       }
> +
> +       d = (struct packed_rrset_data*)p->rrset->entry.data;
> +       if (!packed_rrset_find_rr(d, rdata, rdata_len, &index)) {
> +               ssl_printf(ssl, "error rr %s not found in rrset\n", arg);
> +               return 0;
> +       }
> +
> +       if (!local_rrset_remove_rr(d, index)) {
> +               ssl_printf(ssl, "error unable to delete rr %s\n", arg);
> +               return 0;
> +       }
> +
> +       return 1;
> +}
> +
>  /** Remove RR data */
>  static int
>  perform_data_remove(RES* ssl, struct local_zones* zones, char* arg)
>  {
> -       uint8_t* nm;
> -       int nmlabs;
> -       size_t nmlen;
> -       if(!parse_arg_name(ssl, arg, &nm, &nmlen, &nmlabs))
> +       uint8_t rr[LDNS_RR_BUF_SIZE], *nm;
> +       size_t len = sizeof(rr);
> +       int status, nmlabs;
> +       size_t nmlen, dname_len;
> +
> +       /* try to parse as a rr first */
> +       status = sldns_str2wire_rr_buf(arg, rr, &len, &dname_len, 3600,
> +                               NULL, 0, NULL, 0);
> +
> +       /* try to parse as a domain name second */
> +       if (status != 0) {
> +               if (parse_arg_name(ssl, arg, &nm, &nmlen, &nmlabs)) {
> +                       local_zones_del_data(zones, nm,
> +                               nmlen, nmlabs, LDNS_RR_CLASS_IN);
> +                       free(nm);
> +                       return 1;
> +               }
> +               ssl_printf(ssl, "error cannot parse rr %s at %d: %s\n",
> arg,
> +                       LDNS_WIREPARSE_OFFSET(status),
> +                       sldns_get_errorstr_parse(status));
>                 return 0;
> -       local_zones_del_data(zones, nm,
> -               nmlen, nmlabs, LDNS_RR_CLASS_IN);
> -       free(nm);
> +       }
> +
> +       /* handle the rr case */
> +       if (!perform_data_remove_rr(ssl, zones, rr, len, dname_len, arg))
> +               return 0;
> +
>         return 1;
>  }
>
> @@ -2315,6 +2392,9 @@ zone_del_rrset(struct lruhash_entry* e,
>                         (struct packed_rrset_data*)e->data;
>                 if(d->ttl > inf->expired) {
>                         d->ttl = inf->expired;
> +                       if(d->ttl_add > inf->expired)
> +                               d->ttl_add = inf->expired; /* for 0TTL
> rrsets,
> +                                       means that d->ttl_add <= d->ttl */
>                         inf->num_rrsets++;
>                 }
>         }
> @@ -3238,6 +3318,10 @@ do_auth_zone_reload(RES* ssl, struct wor
>                 return;
>         }
>         if(!auth_zone_read_zonefile(z, worker->env.cfg)) {
> +               /* The old tree was already cleared. Do not answer from the
> +                * failed load. */
> +               z->zone_expired = 1;
> +               auth_zone_clear_data(z);
>                 lock_rw_unlock(&z->lock);
>                 if(xfr) {
>                         lock_basic_unlock(&xfr->lock);
> @@ -3249,6 +3333,7 @@ do_auth_zone_reload(RES* ssl, struct wor
>         z->zone_expired = 0;
>         if(xfr) {
>                 xfr->zone_expired = 0;
> +               xfr->num_ixfrs = 0;
>                 if(!xfr_find_soa(z, xfr)) {
>                         if(z->data.count == 0) {
>                                 lock_rw_unlock(&z->lock);
> @@ -4941,6 +5026,74 @@ fr_check_changed_cfg_str2list(struct con
>         }
>  }
>
> +/** fast reload thread, check if config str3list has changed. */
> +#define FR_CHECK_CHANGED_CFG_STR3LIST(desc, var, buff) do {            \
> +       fr_check_changed_cfg_str3list(cfg->var, newcfg->var, desc, buff,\
> +               sizeof(buff));                                          \
> +       } while(0);
> +static void
> +fr_check_changed_cfg_str3list(struct config_str3list* cmp1,
> +       struct config_str3list* cmp2, const char* desc, char* str, size_t
> len)
> +{
> +       struct config_str3list* p1 = cmp1, *p2 = cmp2;
> +       while(p1 && p2) {
> +               if((!p1->str && p2->str) ||
> +                       (p1->str && !p2->str) ||
> +                       (p1->str && p2->str && strcmp(p1->str, p2->str) !=
> 0)) {
> +                       /* The str3list is different. */
> +                       fr_add_incompatible_option(desc, str, len);
> +                       return;
> +               }
> +               if((!p1->str2 && p2->str2) ||
> +                       (p1->str2 && !p2->str2) ||
> +                       (p1->str2 && p2->str2 &&
> +                       strcmp(p1->str2, p2->str2) != 0)) {
> +                       /* The str3list is different. */
> +                       fr_add_incompatible_option(desc, str, len);
> +                       return;
> +               }
> +               if((!p1->str3 && p2->str3) ||
> +                       (p1->str3 && !p2->str3) ||
> +                       (p1->str3 && p2->str3 &&
> +                       strcmp(p1->str3, p2->str3) != 0)) {
> +                       /* The str3list is different. */
> +                       fr_add_incompatible_option(desc, str, len);
> +                       return;
> +               }
> +               p1 = p1->next;
> +               p2 = p2->next;
> +       }
> +       if((!p1 && p2) || (p1 && !p2)) {
> +               fr_add_incompatible_option(desc, str, len);
> +       }
> +}
> +
> +/** fast reload thread, check tag datas. */
> +static int
> +fr_check_tag_datas(struct fast_reload_thread* fr, struct config_file*
> newcfg)
> +{
> +       char changed_str[1024];
> +       struct config_file* cfg = fr->worker->env.cfg;
> +       changed_str[0]=0;
> +
> +       /* Check for tag_datas in acl_addr. */
> +       FR_CHECK_CHANGED_CFG_STR3LIST("interface-tag-data",
> interface_tag_datas, changed_str);
> +       FR_CHECK_CHANGED_CFG_STR3LIST("access-control-tag-data",
> acl_tag_datas, changed_str);
> +
> +       if(changed_str[0] != 0) {
> +               if(fr->fr_drop_mesh)
> +                       return 1; /* already dropping queries */
> +               fr->fr_drop_mesh = 1;
> +               fr->worker->daemon->fast_reload_drop_mesh =
> fr->fr_drop_mesh;
> +               if(!fr_output_printf(fr, "recursion referenced data has
> changed, with: '%s"
> +                       "', and the queries have to be dropped"
> +                       ", setting '+d'\n", changed_str))
> +                       return 0;
> +               fr_send_notification(fr,
> fast_reload_notification_printout);
> +       }
> +       return 1;
> +}
> +
>  /** fast reload thread, check compatible config items */
>  static int
>  fr_check_compat_cfg(struct fast_reload_thread* fr, struct config_file*
> newcfg)
> @@ -5477,6 +5630,23 @@ xfr_masterlist_equal(struct auth_master*
>         return 0;
>  }
>
> +/** See if configuration has changed. */
> +static int
> +xfr_config_equal(struct auth_xfer* xfr1, struct auth_xfer* xfr2)
> +{
> +       if(xfr1 == NULL && xfr2 == NULL)
> +               return 1;
> +       if(xfr1 == NULL && xfr2 != NULL)
> +               return 0;
> +       if(xfr1 != NULL && xfr2 == NULL)
> +               return 0;
> +       if(xfr1->max_transfer_size != xfr2->max_transfer_size)
> +               return 0;
> +       if(xfr1->max_transfer_time != xfr2->max_transfer_time)
> +               return 0;
> +       return 1;
> +}
> +
>  /** See if the list of masters has changed. */
>  static int
>  xfr_masters_equal(struct auth_xfer* xfr1, struct auth_xfer* xfr2)
> @@ -5565,8 +5735,31 @@ auth_zones_check_changes(struct fast_rel
>                                 &old_serial)!=0);
>                         have_new = (auth_zone_get_serial(new_z,
>                                 &new_serial)!=0);
> +                       /* A change in primaries, also means it is
> different
> +                        * and the change makes it fire new transfers, from
> +                        * the new primaries. */
> +                       /* Treat as changed when the old zone has an
> +                        * outstanding ZONEMD DS/DNSKEY mesh callback.
> +                        * This will make the worker pickup change code
> +                        * remove the mesh callback, before the old zone is
> +                        * deleted. Also it makes a new zonemd lookup.
> +                        * The new lookup is needed, because the new zone
> +                        * entry needs to have a valid zonemd result,
> +                        * and if that is bad, needs to be invalidated.
> +                        * Also if there is a race event where the
> +                        * outstanding callback makes the zone invalid,
> +                        * before fast-reload completes, the change makes
> +                        * the new zone entry have a new zonemd lookup,
> +                        * to then invalidate that new zone.
> +                        * There is also a brief operational window at
> +                        * program start when a zonemd has to be looked
> +                        * up on-line, where the zone is operational.
> +                        * And this copies that for such a race event.
> +                        */
>                         if(have_old != have_new || old_serial != new_serial
> -                               || !xfr_masters_equal(old_xfr, new_xfr)) {
> +                               || !xfr_masters_equal(old_xfr, new_xfr)
> +                               || !xfr_config_equal(old_xfr, new_xfr)
> +                               || old_z->zonemd_callback_env != NULL) {
>                                 /* The zone has been changed. */
>                                 if(!fr_add_auth_zone_change(fr, old_z,
> new_z,
>                                         0, 0, 1)) {
> @@ -5639,6 +5832,8 @@ ct_create_sslctxs(struct fast_reload_con
>                 /* Leave listen ctxs and file str at NULL */
>                 ct->connect_dot_sslctx = daemon_setup_connect_dot_sslctx(
>                         daemon, newcfg);
> +               if(!ct->connect_dot_sslctx)
> +                       return 0;
>                 return 1;
>         }
>
> @@ -5648,20 +5843,28 @@ ct_create_sslctxs(struct fast_reload_con
>                 pem += strlen(chroot);
>
>         ct->listen_dot_sslctx = daemon_setup_listen_dot_sslctx(daemon,
> newcfg);
> +       if(!ct->listen_dot_sslctx)
> +               return 0;
>  #ifdef HAVE_NGHTTP2_NGHTTP2_H
>         if(cfg_has_https(newcfg)) {
>                 ct->listen_doh_sslctx = daemon_setup_listen_doh_sslctx(
>                         daemon, newcfg);
> +               if(!ct->listen_doh_sslctx)
> +                       return 0;
>         }
>  #endif
>  #ifdef HAVE_NGTCP2
>         if(cfg_has_quic(newcfg)) {
>                 ct->listen_quic_sslctx = daemon_setup_listen_quic_sslctx(
>                         daemon, newcfg);
> +               if(!ct->listen_quic_sslctx)
> +                       return 0;
>         }
>  #endif /* HAVE_NGTCP2 */
>         ct->connect_dot_sslctx = daemon_setup_connect_dot_sslctx(daemon,
>                 newcfg);
> +       if(!ct->connect_dot_sslctx)
> +               return 0;
>
>         /* Store mtime and names */
>         ct->ssl_service_key = strdup(newcfg->ssl_service_key);
> @@ -6387,6 +6590,8 @@ fr_atomic_copy_cfg(struct config_file* o
>         COPY_VAR_ptr(ipset_name_v6);
>  #endif
>         COPY_VAR_int(ede);
> +       COPY_VAR_int(val_validation_attempts);
> +       COPY_VAR_int(val_hash_attempts);
>         COPY_VAR_int(iter_scrub_ns);
>         COPY_VAR_int(iter_scrub_cname);
>         COPY_VAR_int(iter_scrub_rrsig);
> @@ -6631,9 +6836,12 @@ fr_reload_config(struct fast_reload_thre
>         }
>  #ifdef USE_DNSTAP
>         if(env->cfg->dnstap) {
> -               if(!fr->fr_nopause)
> -                       dt_apply_cfg(daemon->dtenv, env->cfg);
> -               else dt_apply_logcfg(daemon->dtenv, env->cfg);
> +               if(!fr->fr_nopause) {
> +                       if(!dt_apply_cfg(daemon->dtenv, env->cfg))
> +                               log_warn("fast_reload: dnstap
> identity/version metadata not updated due to allocation failure");
> +               } else {
> +                       dt_apply_logcfg(daemon->dtenv, env->cfg);
> +               }
>         }
>  #endif
>         fr_adjust_cache(env, ct->oldcfg);
> @@ -6773,6 +6981,10 @@ fr_load_config(struct fast_reload_thread
>                 config_delete(newcfg);
>                 return 0;
>         }
> +       if(!fr_check_tag_datas(fr, newcfg)) {
> +               config_delete(newcfg);
> +               return 0;
> +       }
>         if(!fr_check_compat_cfg(fr, newcfg)) {
>                 config_delete(newcfg);
>                 return 0;
> @@ -6864,7 +7076,7 @@ static void* fast_reload_thread_main(voi
>  #endif
>                 log_thread_set(&fast_reload_thread->threadnum);
>
> -       ub_thread_setname(fast_reload_thread->tid, name);
> +       ub_thread_setname(ub_thread_self(), name);
>         (void)name; /* When setname is not defined, ignore the name
> variable. */
>
>         verbose(VERB_ALGO, "start fast reload thread");
> @@ -7587,7 +7799,8 @@ auth_zone_zonemd_stop_lookup(struct auth
>         qinfo.local_alias = NULL;
>
>         mesh_remove_callback(mesh, &qinfo, qflags,
> -               &auth_zonemd_dnskey_lookup_callback, z);
> +               &auth_zonemd_dnskey_lookup_callback, z,
> +               z->zonemd_callback_unique_info);
>  }
>
>  /** Pick up the auth zone locks. */
> @@ -7696,6 +7909,9 @@ auth_xfr_pickup_config(struct auth_xfer*
>         log_assert(loadxfr->namelabs == xfr->namelabs);
>         log_assert(loadxfr->dclass == xfr->dclass);
>
> +       xfr->max_transfer_size = loadxfr->max_transfer_size;
> +       xfr->max_transfer_time =  loadxfr->max_transfer_time;
> +
>         /* The lists can be swapped in, the other xfr struct will be
> deleted
>          * afterwards. */
>         probe_masters = xfr->task_probe->masters;
> @@ -7720,6 +7936,16 @@ fr_worker_auth_add(struct worker* worker
>                 /* The xfr item needs to be created. The auth zones lock
>                  * is held to make this possible. */
>                 xfr = auth_xfer_create(worker->env.auth_zones,
> item->new_z);
> +               if(!xfr) {
> +                       log_err("out of memory in fr_worker_auth_add");
> +                       lock_rw_unlock(&item->new_z->lock);
> +                       lock_rw_unlock(&worker->env.auth_zones->lock);
> +
>  lock_rw_unlock(&worker->daemon->fast_reload_thread->old_auth_zones->lock);
> +                       if(loadxfr) {
> +                               lock_basic_unlock(&loadxfr->lock);
> +                       }
> +                       return;
> +               }
>                 auth_xfr_pickup_config(loadxfr, xfr);
>                 /* Serial information is copied into the xfr struct. */
>                 if(!xfr_find_soa(item->new_z, xfr)) {
> @@ -7789,6 +8015,17 @@ fr_worker_auth_cha(struct worker* worker
>         } else if(loadxfr && !xfr) {
>                 /* Create the xfr. */
>                 xfr = auth_xfer_create(worker->env.auth_zones,
> item->new_z);
> +               if(!xfr) {
> +                       log_err("out of memory in fr_worker_auth_cha");
> +                       lock_rw_unlock(&item->new_z->lock);
> +                       lock_rw_unlock(&item->old_z->lock);
> +
>  lock_rw_unlock(&worker->daemon->fast_reload_thread->old_auth_zones->lock);
> +                       lock_rw_unlock(&worker->env.auth_zones->lock);
> +                       if(loadxfr) {
> +                               lock_basic_unlock(&loadxfr->lock);
> +                       }
> +                       return;
> +               }
>                 auth_xfr_pickup_config(loadxfr, xfr);
>                 item->new_z->zone_is_slave = 1;
>         }
> Index: daemon/remote.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/remote.h,v
> diff -u -p -r1.11 remote.h
> --- daemon/remote.h     26 May 2026 11:14:11 -0000      1.11
> +++ daemon/remote.h     20 Sep 2026 09:50:47 -0000
> @@ -49,6 +49,7 @@
>  #include <openssl/ssl.h>
>  #endif
>  #include "util/locks.h"
> +#include "libunbound/remote.h"
>  struct config_file;
>  struct listen_list;
>  struct listen_port;
> @@ -364,13 +365,6 @@ void fast_reload_thread_start(RES* ssl,
>   * @param fast_reload_thread: the thread struct.
>   */
>  void fast_reload_thread_stop(struct fast_reload_thread*
> fast_reload_thread);
> -
> -/** fast reload thread commands to remote service thread event callback */
> -void fast_reload_service_cb(int fd, short bits, void* arg);
> -
> -/** fast reload callback for the remote control client connection */
> -int fast_reload_client_callback(struct comm_point* c, void* arg, int err,
> -       struct comm_reply* rep);
>
>  /** fast reload printq delete list */
>  void fast_reload_printq_list_delete(struct fast_reload_printq* list);
> Index: daemon/stats.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/stats.c,v
> diff -u -p -r1.20 stats.c
> --- daemon/stats.c      26 May 2026 11:14:11 -0000      1.20
> +++ daemon/stats.c      20 Sep 2026 09:50:47 -0000
> @@ -422,12 +422,28 @@ void server_stats_obtain(struct worker*
>  #  endif
>  #endif
>                         );
> +               log_err("server_stats_obtain: no response from worker %d "
> +                       "(stats timeout); returning zero stats for this
> worker",
> +                       who->thread_num);
> +               /* A later reply from the worker, would be sizeof stats
> reply,
> +                * and the worker_handle_control_cmd routine discards if
> +                * it is not a 4byte command, when that is received here.
> */
> +               memset(s, 0, sizeof(*s));
> +               return;
> +       }
> +       if(!tube_read_msg(worker->cmd, &reply, &len, 0)) {
> +               log_err("server_stats_obtain: failed to read stats from
> worker "
> +                       "(tube read error); returning zero stats for this
> worker");
> +               memset(s, 0, sizeof(*s));
> +               return;
> +       }
> +       if(len != (uint32_t)sizeof(*s)) {
> +               log_err("server_stats_obtain: wrong stats length %d
> (expected %d); "
> +                       "discarding", (int)len, (int)sizeof(*s));
> +               free(reply);
> +               memset(s, 0, sizeof(*s));
> +               return;
>         }
> -       if(!tube_read_msg(worker->cmd, &reply, &len, 0))
> -               fatal_exit("failed to read stats over cmd channel");
> -       if(len != (uint32_t)sizeof(*s))
> -               fatal_exit("stats on cmd channel wrong length %d %d",
> -                       (int)len, (int)sizeof(*s));
>         memcpy(s, reply, (size_t)len);
>         free(reply);
>  }
> @@ -439,7 +455,7 @@ void server_stats_reply(struct worker* w
>         verbose(VERB_ALGO, "write stats replymsg");
>         if(!tube_write_msg(worker->daemon->workers[0]->cmd,
>                 (uint8_t*)&s, sizeof(s), 0))
> -               fatal_exit("could not write stat values over cmd channel");
> +               log_err("could not write stat values over cmd channel");
>  }
>
>  void server_stats_add(struct ub_stats_info* total, struct ub_stats_info*
> a)
> Index: daemon/worker.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/daemon/worker.c,v
> diff -u -p -r1.46 worker.c
> --- daemon/worker.c     27 Jul 2026 14:14:39 -0000      1.46
> +++ daemon/worker.c     20 Sep 2026 09:50:47 -0000
> @@ -501,7 +501,9 @@ worker_handle_control_cmd(struct tube* A
>                 return;
>         }
>         if(len != sizeof(uint32_t)) {
> -               fatal_exit("bad control msg length %d", (int)len);
> +               verbose(VERB_ALGO, "bad control msg length %d", (int)len);
> +               free(msg);
> +               return;
>         }
>         cmd = sldns_read_uint32(msg);
>         free(msg);
> @@ -714,7 +716,8 @@ apply_respip_action(struct worker* worke
>         struct respip_client_info* cinfo, struct reply_info* rep,
>         struct sockaddr_storage* addr, socklen_t addrlen,
>         struct ub_packed_rrset_key** alias_rrset,
> -       struct reply_info** encode_repp, struct auth_zones* az)
> +       struct reply_info** encode_repp, struct auth_zones* az,
> +       int* rpz_passthru)
>  {
>         struct respip_action_info actinfo = {0, 0, 0, 0, NULL, 0, NULL};
>         actinfo.action = respip_none;
> @@ -725,7 +728,7 @@ apply_respip_action(struct worker* worke
>                 return 1;
>
>         if(!respip_rewrite_reply(qinfo, cinfo, rep, encode_repp, &actinfo,
> -               alias_rrset, 0, worker->scratchpad, az, NULL,
> +               alias_rrset, 0, worker->scratchpad, az, rpz_passthru,
>                 worker->env.views, worker->env.respip_set))
>                 return 0;
>
> @@ -772,7 +775,7 @@ answer_from_cache(struct worker* worker,
>         int* is_secure_answer, struct ub_packed_rrset_key** alias_rrset,
>         struct reply_info** partial_repp,
>         struct reply_info* rep, uint16_t id, uint16_t flags,
> -       struct comm_reply* repinfo, struct edns_data* edns)
> +       struct comm_reply* repinfo, struct edns_data* edns, int*
> rpz_passthru)
>  {
>         time_t timenow = *worker->env.now;
>         uint16_t udpsize = edns->udp_size;
> @@ -860,7 +863,7 @@ answer_from_cache(struct worker* worker,
>                         "validation");
>                 goto bail_out; /* need to validate cache entry first */
>         } else if(rep->security == sec_status_secure) {
> -               if(reply_all_rrsets_secure(rep)) {
> +               if(reply_an_ns_rrsets_secure(rep)) {
>                         *is_secure_answer = 1;
>                 } else {
>                         if(must_validate) {
> @@ -882,7 +885,7 @@ answer_from_cache(struct worker* worker,
>         if((worker->daemon->use_response_ip || worker->daemon->use_rpz) &&
>                 !partial_rep && !apply_respip_action(worker, qinfo, cinfo,
> rep,
>                 &repinfo->client_addr, repinfo->client_addrlen,
> alias_rrset,
> -               &encode_rep, worker->env.auth_zones)) {
> +               &encode_rep, worker->env.auth_zones, rpz_passthru)) {
>                 goto bail_out;
>         } else if(partial_rep &&
>                 !respip_merge_cname(partial_rep, qinfo, rep, cinfo,
> @@ -1494,6 +1497,8 @@ worker_handle_request(struct comm_point*
>         struct reply_info* partial_rep = NULL;
>         struct query_info* lookup_qinfo = &qinfo;
>         struct query_info qinfo_tmp; /* placeholder for lookup_qinfo */
> +       uint8_t* alias_orig_qname = NULL; /* original qname for logs, if
> +               a local_alias is used to change the qname. */
>         struct respip_client_info* cinfo = NULL, cinfo_tmp;
>         struct timeval wait_time;
>         struct check_request_result check_result = {0,0};
> @@ -1511,7 +1516,7 @@ worker_handle_request(struct comm_point*
>                 if (worker->stats.max_query_time_us < wait_queue_time)
>                         worker->stats.max_query_time_us = wait_queue_time;
>                 if(wait_queue_time >
> -                       (long long)(worker->env.cfg->sock_queue_timeout *
> 1000000)) {
> +                       (long long)worker->env.cfg->sock_queue_timeout *
> 1000000) {
>                         /* count and drop queries that were sitting in the
> socket queue too long */
>                         worker->stats.num_queries_timed_out++;
>                         return 0;
> @@ -1936,6 +1941,11 @@ worker_handle_request(struct comm_point*
>         /* If we've found a local alias, replace the qname with the alias
>          * target before resolving it. */
>         if(qinfo.local_alias) {
> +               if(qinfo.local_alias->rrset &&
> +                       qinfo.local_alias->rrset->rk.dname)
> +                       /* Store the original qname, used for logs, since
> +                        * local_alias can be removed by region_free_all.
> */
> +                       alias_orig_qname =
> qinfo.local_alias->rrset->rk.dname;
>                 if(!local_alias_shallow_copy_qname(qinfo.local_alias,
> &qinfo.qname,
>                         &qinfo.qname_len)) {
>                         regional_free_all(worker->scratchpad);
> @@ -1983,7 +1993,7 @@ lookup_cache:
>                                 &alias_rrset, &partial_rep, rep,
>                                 *(uint16_t*)(void
> *)sldns_buffer_begin(c->buffer),
>                                 sldns_buffer_read_u16_at(c->buffer, 2),
> repinfo,
> -                               &edns)) {
> +                               &edns, &rpz_passthru)) {
>                                 /* prefetch it if the prefetch TTL expired.
>                                  * Note that if there is more than one pass
>                                  * its qname must be that used for cache
> @@ -2101,11 +2111,10 @@ send_reply_rc:
>         {
>                 struct timeval tv;
>                 memset(&tv, 0, sizeof(tv));
> -               if(qinfo.local_alias && qinfo.local_alias->rrset &&
> -                       qinfo.local_alias->rrset->rk.dname) {
> +               if(alias_orig_qname) {
>                         /* log original qname, before the local alias was
>                          * used to resolve that CNAME to something else */
> -                       qinfo.qname = qinfo.local_alias->rrset->rk.dname;
> +                       qinfo.qname = alias_orig_qname;
>                         log_reply_info(NO_VERBOSE, &qinfo,
>                                 &repinfo->client_addr,
> repinfo->client_addrlen,
>                                 tv, 1, c->buffer,
> @@ -2374,6 +2383,8 @@ worker_init(struct worker* worker, struc
>                 worker_stat_timer_cb, worker);
>         if(!worker->stat_timer) {
>                 log_err("could not create statistics timer");
> +               worker_delete(worker);
> +               return 0;
>         }
>
>         /* we use the msg_buffer_size as a good estimate for what the
> @@ -2526,6 +2537,8 @@ worker_delete(struct worker* worker)
>         /* don't touch worker->alloc, as it's maintained in daemon */
>         regional_destroy(worker->env.scratch);
>         regional_destroy(worker->scratchpad);
> +       /* The thread id can reference this worker's id value, so clear
> it. */
> +       log_thread_set(NULL);
>         free(worker);
>  }
>
> @@ -2534,7 +2547,8 @@ worker_send_query(struct query_info* qin
>         int want_dnssec, int nocaps, int check_ratelimit,
>         struct sockaddr_storage* addr, socklen_t addrlen, uint8_t* zone,
>         size_t zonelen, int tcp_upstream, int ssl_upstream, char*
> tls_auth_name,
> -       struct module_qstate* q, int* was_ratelimited)
> +       struct module_qstate* q, int* was_ratelimited,
> +       int* ratelimit_incremented)
>  {
>         struct worker* worker = q->env->worker;
>         struct outbound_entry* e = (struct outbound_entry*)regional_alloc(
> @@ -2546,7 +2560,7 @@ worker_send_query(struct query_info* qin
>                 want_dnssec, nocaps, check_ratelimit, tcp_upstream,
>                 ssl_upstream, tls_auth_name, addr, addrlen, zone, zonelen,
> q,
>                 worker_handle_service_reply, e, worker->back->udp_buff,
> q->env,
> -               was_ratelimited);
> +               was_ratelimited, ratelimit_incremented);
>         if(!e->qsent) {
>                 return NULL;
>         }
> @@ -2595,7 +2609,8 @@ struct outbound_entry* libworker_send_qu
>         struct sockaddr_storage* ATTR_UNUSED(addr), socklen_t
> ATTR_UNUSED(addrlen),
>         uint8_t* ATTR_UNUSED(zone), size_t ATTR_UNUSED(zonelen), int
> ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> Index: dns64/dns64.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dns64/dns64.c,v
> diff -u -p -r1.25 dns64.c
> --- dns64/dns64.c       26 May 2026 11:14:11 -0000      1.25
> +++ dns64/dns64.c       20 Sep 2026 09:50:47 -0000
> @@ -643,6 +643,12 @@ handle_event_moddone(struct module_qstat
>                 qstate->return_msg->rep &&
>                 reply_find_answer_rrset(&qstate->qinfo,
> qstate->return_msg->rep);
>         int synth_qname = 0;
> +       if(could_synth && !has_data && qstate->env->need_to_validate &&
> +               qstate->return_msg && qstate->return_msg->rep &&
> +               qstate->return_msg->rep->security == sec_status_bogus) {
> +               verbose(VERB_ALGO, "dns64: bogus AAAA reply not
> synthesized");
> +               could_synth = 0;
> +       }
>
>         if(could_synth &&
>                 (!has_data ||
> @@ -654,8 +660,11 @@ handle_event_moddone(struct module_qstat
>
>         /* Store the response in cache. */
>         if( (!iq || !iq->started_no_cache_store) &&
> +               !qstate->rpz_applied && !qstate->rpz_passthru &&
> +               !qstate->is_subnet_answer &&
>                 qstate->return_msg &&
>                 qstate->return_msg->rep &&
> +               !qstate->fwd_stub_no_cache &&
>                 !dns_cache_store(
>                         qstate->env, &qstate->qinfo,
> qstate->return_msg->rep,
>                         0, qstate->prefetch_leeway, 0, NULL,
> @@ -717,8 +726,15 @@ dns64_operate(struct module_qstate* qsta
>         }
>         if(qstate->ext_state[id] == module_finished) {
>                 iq = (struct dns64_qstate*)qstate->minfo[id];
> -               if(iq && iq->state != DNS64_INTERNAL_QUERY)
> -                       qstate->no_cache_store =
> iq->started_no_cache_store;
> +               if(iq && iq->state != DNS64_INTERNAL_QUERY) {
> +                       if(qstate->fwd_stub_no_cache) {
> +                               /* If the forward/stub has no cache, then
> +                                * continue with the query with no cache.
> */
> +                               qstate->no_cache_store =
> qstate->fwd_stub_no_cache;
> +                       } else {
> +                               qstate->no_cache_store =
> iq->started_no_cache_store;
> +                       }
> +               }
>         }
>  }
>
> @@ -825,6 +841,7 @@ dns64_adjust_a(int id, struct module_qst
>         size_t i, s;
>         struct packed_rrset_data* fd, *dd;
>         struct ub_packed_rrset_key* fk, *dk;
> +       int allocated_return_msg = 0;
>
>         verbose(VERB_ALGO, "converting A answers to AAAA answers");
>
> @@ -840,6 +857,7 @@ dns64_adjust_a(int id, struct module_qst
>                         return;
>                 memset(super->return_msg, 0, sizeof(*super->return_msg));
>                 super->return_msg->qinfo = super->qinfo;
> +               allocated_return_msg = 1;
>         }
>
>         rep = qstate->return_msg->rep;
> @@ -852,11 +870,14 @@ dns64_adjust_a(int id, struct module_qst
>                 rep->serve_expired_norec_ttl,
>                 rep->an_numrrsets, rep->ns_numrrsets, rep->ar_numrrsets,
>                 rep->rrset_count, rep->security, LDNS_EDE_NONE);
> -       if(!cp)
> +       if(!cp) {
> +               if(allocated_return_msg) super->return_msg = NULL;
>                 return;
> +       }
>
>         /* allocate ub_key structures special or not */
>         if(!reply_info_alloc_rrset_keys(cp, NULL, super->region)) {
> +               if(allocated_return_msg) super->return_msg = NULL;
>                 return;
>         }
>
> @@ -871,8 +892,10 @@ dns64_adjust_a(int id, struct module_qst
>                 if(i<rep->an_numrrsets && fk->rk.type ==
> htons(LDNS_RR_TYPE_A)) {
>                         /* also sets dk->entry.hash */
>                         dns64_synth_aaaa_data(fk, fd, dk, &dd,
> super->region, dns64_env);
> -                       if(!dd)
> +                       if(!dd) {
> +                               if(allocated_return_msg) super->return_msg
> = NULL;
>                                 return;
> +                       }
>                         /* Delete negative AAAA record from cache stored by
>                          * the iterator module */
>                         rrset_cache_remove(super->env->rrset_cache,
> dk->rk.dname,
> @@ -889,15 +912,19 @@ dns64_adjust_a(int id, struct module_qst
>                         dk->rk.dname =
> (uint8_t*)regional_alloc_init(super->region,
>                                 fk->rk.dname, fk->rk.dname_len);
>
> -                       if(!dk->rk.dname)
> +                       if(!dk->rk.dname) {
> +                               if(allocated_return_msg) super->return_msg
> = NULL;
>                                 return;
> +                       }
>
>                         s = packed_rrset_sizeof(fd);
>                         dd = (struct
> packed_rrset_data*)regional_alloc_init(
>                                 super->region, fd, s);
>
> -                       if(!dd)
> +                       if(!dd) {
> +                               if(allocated_return_msg) super->return_msg
> = NULL;
>                                 return;
> +                       }
>                 }
>
>                 packed_rrset_ptr_fixup(dd);
> @@ -928,8 +955,10 @@ dns64_adjust_ptr(struct module_qstate* q
>          return;
>      super->return_msg->qinfo = super->qinfo;
>      if (!(super->return_msg->rep =
> reply_info_copy(qstate->return_msg->rep,
> -                    NULL, super->region)))
> +                    NULL, super->region))) {
> +       super->return_msg = NULL;
>          return;
> +    }
>
>      /*
>       * Adjust the domain name of the answer RR set so that it matches the
> @@ -998,6 +1027,21 @@ dns64_inform_super(struct module_qstate*
>         /* Use return code from A query in response to client. */
>         if (super->return_rcode != LDNS_RCODE_NOERROR)
>                 super->return_rcode = qstate->return_rcode;
> +       /* RPZ applied to the subquery need to then change (not cache)
> +        * the super query. With the super query not cached, it is
> +        * going to run the state machine modules on incoming queries,
> +        * that fetch the subquery (cache) response, and modify it
> +        * according to the rpz policy. That makes the synthesized
> +        * super query also adjusted by rpz policies. But loses cache
> +        * hits. Even though the subquery likely is answered from cache,
> +        * internally in its state machine process. */
> +       if(qstate->rpz_applied)
> +               super->rpz_applied = 1;
> +       if(qstate->rpz_passthru)
> +               super->rpz_passthru = 1;
> +
> +       /* Since the super qstate has a new response, its errinf is
> removed. */
> +       super->errinf = NULL;
>
>         /* Generate a response suitable for the original query. */
>         if (qstate->qinfo.qtype == LDNS_RR_TYPE_A) {
> @@ -1006,9 +1050,16 @@ dns64_inform_super(struct module_qstate*
>                 log_assert(qstate->qinfo.qtype == LDNS_RR_TYPE_PTR);
>                 dns64_adjust_ptr(qstate, super);
>         }
> +       /* If the sub-query has no cache store, then also the super query.
> */
> +       if(qstate->fwd_stub_no_cache)
> +               super->fwd_stub_no_cache = 1;
>
>         /* Store the generated response in cache. */
> -       if ( (!super_dq || !super_dq->started_no_cache_store) &&
> +       if ( super->return_msg && super->return_msg->rep &&
> +               (!super_dq || !super_dq->started_no_cache_store) &&
> +               !qstate->fwd_stub_no_cache &&
> +               !super->rpz_applied && !super->rpz_passthru &&
> +               !super->is_subnet_answer &&
>                 !dns_cache_store(super->env, &super->qinfo,
> super->return_msg->rep,
>                 0, super->prefetch_leeway, 0, NULL, super->query_flags,
>                 qstate->qstarttime, qstate->is_valrec))
> Index: dnscrypt/dnscrypt.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dnscrypt/dnscrypt.c,v
> diff -u -p -r1.10 dnscrypt.c
> --- dnscrypt/dnscrypt.c 27 Jul 2026 14:14:39 -0000      1.10
> +++ dnscrypt/dnscrypt.c 20 Sep 2026 09:50:47 -0000
> @@ -842,7 +842,14 @@ dnsc_parse_keys(struct dnsc_env *env, st
>                         if(memcmp(current_keypair->crypt_publickey,
>                                 env->signed_certs[c].server_publickey,
>                                 crypto_box_PUBLICKEYBYTES) == 0) {
> -                               dnsccert *current_cert =
> &env->certs[cert_id++];
> +                               dnsccert* current_cert;
> +                               if(cert_id >= env->signed_certs_count) {
> +                                       log_err("dnscrypt: secret key %s
> matches a cert that "
> +                                               "is already bound to
> another key (duplicate "
> +                                               "dnscrypt-secret-key?)",
> head->str);
> +                                       return -1;
> +                               }
> +                               current_cert = &env->certs[cert_id++];
>                                 found_cert = 1;
>                                 current_cert->keypair = current_keypair;
>                                 memcpy(current_cert->magic_query,
> Index: dnstap/dnstap.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dnstap/dnstap.c,v
> diff -u -p -r1.14 dnstap.c
> --- dnstap/dnstap.c     26 Sep 2025 07:32:37 -0000      1.14
> +++ dnstap/dnstap.c     20 Sep 2026 09:50:47 -0000
> @@ -176,26 +176,29 @@ dt_create(struct config_file* cfg)
>         env->dtio = dt_io_thread_create();
>         if(!env->dtio) {
>                 log_err("malloc failure");
> -               free(env);
> +               dt_delete(env);
>                 return NULL;
>         }
>         if(!dt_io_thread_apply_cfg(env->dtio, cfg)) {
> -               dt_io_thread_delete(env->dtio);
> -               free(env);
> +               dt_delete(env);
> +               return NULL;
> +       }
> +       if(!dt_apply_cfg(env, cfg)) {
> +               dt_delete(env);
>                 return NULL;
>         }
> -       dt_apply_cfg(env, cfg);
>         return env;
>  }
>
> -static void
> +static int
>  dt_apply_identity(struct dt_env *env, struct config_file *cfg)
>  {
>         char buf[MAXHOSTNAMELEN+1];
>         if (!cfg->dnstap_send_identity) {
>                 free(env->identity);
>                 env->identity = NULL;
> -               return;
> +               env->len_identity = 0;
> +               return 1;
>         }
>         free(env->identity);
>         if (cfg->dnstap_identity == NULL || cfg->dnstap_identity[0] == 0) {
> @@ -203,36 +206,49 @@ dt_apply_identity(struct dt_env *env, st
>                         buf[MAXHOSTNAMELEN] = 0;
>                         env->identity = strdup(buf);
>                 } else {
> -                       fatal_exit("dt_apply_identity: gethostname()
> failed");
> +                       log_err("dt_apply_identity: gethostname() failed:
> %s",
> +                               strerror(errno));
> +                       env->identity = NULL;
> +                       env->len_identity = 0;
> +                       return 0;
>                 }
>         } else {
>                 env->identity = strdup(cfg->dnstap_identity);
>         }
> -       if (env->identity == NULL)
> -               fatal_exit("dt_apply_identity: strdup() failed");
> +       if (env->identity == NULL) {
> +               log_err("dt_apply_identity: strdup() failed");
> +               env->len_identity = 0;
> +               return 0;
> +       }
>         env->len_identity = (unsigned int)strlen(env->identity);
>         verbose(VERB_OPS, "dnstap identity field set to \"%s\"",
>                 env->identity);
> +       return 1;
>  }
>
> -static void
> +static int
>  dt_apply_version(struct dt_env *env, struct config_file *cfg)
>  {
>         if (!cfg->dnstap_send_version) {
>                 free(env->version);
>                 env->version = NULL;
> -               return;
> +               env->len_version = 0;
> +               return 1;
>         }
>         free(env->version);
>         if (cfg->dnstap_version == NULL || cfg->dnstap_version[0] == 0)
>                 env->version = strdup(PACKAGE_STRING);
>         else
>                 env->version = strdup(cfg->dnstap_version);
> -       if (env->version == NULL)
> -               fatal_exit("dt_apply_version: strdup() failed");
> +       if (env->version == NULL) {
> +               log_err("dt_apply_version: strdup() failed");
> +               env->len_version = 0;
> +               return 0;
> +       }
>         env->len_version = (unsigned int)strlen(env->version);
>         verbose(VERB_OPS, "dnstap version field set to \"%s\"",
>                 env->version);
> +       return 1;
>  }
>
>  void
> @@ -276,15 +292,18 @@ dt_apply_logcfg(struct dt_env *env, stru
>         lock_basic_unlock(&env->sample_lock);
>  }
>
> -void
> +int
>  dt_apply_cfg(struct dt_env *env, struct config_file *cfg)
>  {
>         if (!cfg->dnstap)
> -               return;
> +               return 1;
>
> -       dt_apply_identity(env, cfg);
> -       dt_apply_version(env, cfg);
>         dt_apply_logcfg(env, cfg);
> +       if(!dt_apply_identity(env, cfg))
> +               return 0;
> +       if(!dt_apply_version(env, cfg))
> +               return 0;
> +       return 1;
>  }
>
>  int
> Index: dnstap/dnstap.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dnstap/dnstap.h,v
> diff -u -p -r1.1.1.8 dnstap.h
> --- dnstap/dnstap.h     31 Aug 2025 21:36:34 -0000      1.1.1.8
> +++ dnstap/dnstap.h     20 Sep 2026 09:50:47 -0000
> @@ -102,9 +102,9 @@ dt_create(struct config_file* cfg);
>   * Apply config settings.
>   * @param env: dnstap environment object.
>   * @param cfg: new config settings.
> + * @return false on failure.
>   */
> -void
> -dt_apply_cfg(struct dt_env *env, struct config_file *cfg);
> +int dt_apply_cfg(struct dt_env *env, struct config_file *cfg);
>
>  /**
>   * Apply config settings for log enable for message types.
> Index: dnstap/dtstream.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dnstap/dtstream.c,v
> diff -u -p -r1.4 dtstream.c
> --- dnstap/dtstream.c   26 May 2026 11:14:11 -0000      1.4
> +++ dnstap/dtstream.c   20 Sep 2026 09:50:47 -0000
> @@ -222,7 +222,7 @@ dt_msg_queue_start_timer(struct dt_msg_q
>                 tv.tv_usec = 0;
>                 /* If it is already set, keep it running. */
>                 if(!comm_timer_is_set(mq->wakeup_timer))
> -       comm_timer_set(mq->wakeup_timer, &tv);
> +                       comm_timer_set(mq->wakeup_timer, &tv);
>         } else {
>                 tv.tv_sec = 0;
>                 tv.tv_usec = 0;
> @@ -1554,7 +1554,7 @@ void dtio_output_cb(int ATTR_UNUSED(fd),
>                                         }
>                                 }
>                                 if(!dtio->cur_msg)
> -                               return; /* nothing to do */
> +                                       return; /* nothing to do */
>                         }
>                 }
>
> @@ -2144,7 +2144,7 @@ static void* dnstap_io(void* arg)
>  #endif
>                 log_thread_set(&dtio->threadnum);
>
> -       ub_thread_setname(dtio->tid, name);
> +       ub_thread_setname(ub_thread_self(), name);
>
>         /* setup */
>         verbose(VERB_ALGO, "start dnstap io thread");
> Index: dnstap/unbound-dnstap-socket.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/dnstap/unbound-dnstap-socket.c,v
> diff -u -p -r1.6 unbound-dnstap-socket.c
> --- dnstap/unbound-dnstap-socket.c      27 Jul 2026 14:14:39 -0000      1.6
> +++ dnstap/unbound-dnstap-socket.c      20 Sep 2026 09:50:47 -0000
> @@ -75,18 +75,18 @@
>  static void usage(char* argv[])
>  {
>         printf("usage: %s [options]\n", argv[0]);
> -       printf("        Listen to dnstap messages\n");
> +       printf("        Listen to dnstap messages\n");
>         printf("stdout has dnstap log, stderr has verbose server log\n");
> -       printf("-u <socketpath> listen to unix socket with this file
> name\n");
> -       printf("-s <serverip[@port]> listen for TCP on the IP and port\n");
> -       printf("-t <serverip[@port]> listen for TLS on IP and port\n");
> -       printf("-x <server.key> server key file for TLS service\n");
> -       printf("-y <server.pem> server cert file for TLS service\n");
> -       printf("-z <verify.pem> cert file to verify client connections\n");
> -       printf("-l              long format for DNS printout\n");
> -       printf("-v              more verbose log output\n");
> +       printf("-u <socketpath>         listen to unix socket with this
> file name\n");
> +       printf("-s <serverip[@port]>    listen for TCP on the IP and
> port\n");
> +       printf("-t <serverip[@port]>    listen for TLS on IP and port\n");
> +       printf("-x <server.key>         server key file for TLS
> service\n");
> +       printf("-y <server.pem>         server cert file for TLS
> service\n");
> +       printf("-z <verify.pem>         cert file to verify client
> connections\n");
> +       printf("-l                      long format for DNS printout\n");
> +       printf("-v                      more verbose log output\n");
>         printf("-c                      internal unit test and exit\n");
> -       printf("-h              this help text\n");
> +       printf("-h                      this help text\n");
>         exit(1);
>  }
>
> @@ -1659,7 +1659,8 @@ struct outbound_entry* worker_send_query
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> @@ -1693,7 +1694,8 @@ struct outbound_entry* libworker_send_qu
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> Index: doc/README
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/README,v
> diff -u -p -r1.47 README
> --- doc/README  27 Jul 2026 14:14:39 -0000      1.47
> +++ doc/README  20 Sep 2026 09:50:48 -0000
> @@ -1,4 +1,4 @@
> -README for Unbound 1.25.2
> +README for Unbound 1.26.1
>  Copyright 2007 NLnet Labs
>  http://unbound.net
>
> Index: doc/example.conf.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/example.conf.in,v
> diff -u -p -r1.52 example.conf.in
> --- doc/example.conf.in 27 Jul 2026 14:14:39 -0000      1.52
> +++ doc/example.conf.in 20 Sep 2026 09:50:48 -0000
> @@ -1,7 +1,7 @@
>  #
>  # Example configuration file.
>  #
> -# See unbound.conf(5) man page, version 1.25.2.
> +# See unbound.conf(5) man page, version 1.26.1.
>  #
>  # this is a comment.
>
> @@ -203,6 +203,12 @@ server:
>         # protects against poison attempts.
>         # iter-scrub-promiscuous: yes
>
> +       # Limit on number of DNSSEC validation attempts for a query.
> +       # val-validation-attempts: 32
> +
> +       # Limit on number of DNSSEC hash attempts for a query.
> +       # val-hash-attempts: 32
> +
>         # 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
> @@ -728,7 +734,7 @@ server:
>         # non-secure data. Useful to shield the users of this validator
> from
>         # potential bogus data in the additional section. All unsigned data
>         # in the additional section is removed from secure messages.
> -       # val-clean-additional: yes
> +       # val-clean-additional: no
>
>         # Turn permissive mode on to permit bogus messages. Thus, messages
>         # for which security checks failed will be returned to clients,
> @@ -899,6 +905,10 @@ server:
>         #   that name
>         # o block_a resolves all records normally but returns
>         #   NODATA for A queries and ignores local data for that name
> +       # o block_aaaa similarly to block_a, resolves all records normally
> but
> +       #   returns NODATA for AAAA queries and ignores local data for
> that name
> +       # o block_a_wdata like block_a but uses local data if present.
> +       # o block_aaaa_wdata like block_aaaa but uses local data if
> present.
>         # o always_null returns 0.0.0.0 or ::0 for any name in the zone.
>         # o noview breaks out of that view towards global local-zones.
>         #
> @@ -1287,6 +1297,9 @@ remote-control:
>  #      zonemd-check: no
>  #      zonemd-reject-absence: no
>  #      zonefile: "example.org.zone"
> +#      max-transfer-size: 0
> +#      max-transfer-time: 0
> +
>
>  # Views
>  # Create named views. Name must be unique.
> @@ -1453,3 +1466,5 @@ remote-control:
>  #     rpz-signal-nxdomain-ra: no
>  #     for-downstream: no
>  #     tags: "example"
> +#     max-transfer-size: 0
> +#     max-transfer-time: 0
> Index: doc/libunbound.3.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/libunbound.3.in,v
> diff -u -p -r1.50 libunbound.3.in
> --- doc/libunbound.3.in 27 Jul 2026 14:14:39 -0000      1.50
> +++ doc/libunbound.3.in 20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "LIBUNBOUND" "3" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "LIBUNBOUND" "3" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -libunbound \- Unbound DNS validating resolver 1.25.2 functions.
> +libunbound \- Unbound DNS validating resolver 1.26.1 functions.
>  .SH SYNOPSIS
>  .sp
>  \fB#include <unbound.h>\fP
> Index: doc/unbound-anchor.8.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-anchor.8.in,v
> diff -u -p -r1.49 unbound-anchor.8.in
> --- doc/unbound-anchor.8.in     27 Jul 2026 14:14:39 -0000      1.49
> +++ doc/unbound-anchor.8.in     20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND-ANCHOR" "8" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND-ANCHOR" "8" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound-anchor \- Unbound 1.25.2 anchor utility.
> +unbound-anchor \- Unbound 1.26.1 anchor utility.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound\-anchor\fP [\fBopts\fP]
> Index: doc/unbound-checkconf.8.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-checkconf.8.in,v
> diff -u -p -r1.49 unbound-checkconf.8.in
> --- doc/unbound-checkconf.8.in  27 Jul 2026 14:14:39 -0000      1.49
> +++ doc/unbound-checkconf.8.in  20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND-CHECKCONF" "8" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND-CHECKCONF" "8" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound-checkconf \- Check Unbound 1.25.2 configuration file for errors.
> +unbound-checkconf \- Check Unbound 1.26.1 configuration file for errors.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound\-checkconf\fP [\fB\-hf\fP] [\fB\-o option\fP] [cfgfile]
> Index: doc/unbound-control.8.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-control.8.in,v
> diff -u -p -r1.51 unbound-control.8.in
> --- doc/unbound-control.8.in    27 Jul 2026 14:14:39 -0000      1.51
> +++ doc/unbound-control.8.in    20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND-CONTROL" "8" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND-CONTROL" "8" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound-control \- Unbound 1.25.2 remote server control utility.
> +unbound-control \- Unbound 1.26.1 remote server control utility.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound\-control\fP [\fB\-hq\fP] [\fB\-c cfgfile\fP] [\fB\-s
> server\fP] command
> @@ -354,6 +354,8 @@ If the name already has no items, nothin
>  Often results in NXDOMAIN for the name (in a static zone), but if the name
>  has become an empty nonterminal (there is still data in domain names below
>  the removed name), NOERROR nodata answers are the result for that name.
> +With a specific RR instead of a domain name, that specific record is
> +removed from the local data, and not all the RR data.
>  .UNINDENT
>  .INDENT 0.0
>  .TP
> Index: doc/unbound-control.rst
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-control.rst,v
> diff -u -p -r1.1.1.3 unbound-control.rst
> --- doc/unbound-control.rst     26 May 2026 11:10:50 -0000      1.1.1.3
> +++ doc/unbound-control.rst     20 Sep 2026 09:50:48 -0000
> @@ -347,6 +347,8 @@ There are several commands that the serv
>      Often results in NXDOMAIN for the name (in a static zone), but if the
> name
>      has become an empty nonterminal (there is still data in domain names
> below
>      the removed name), NOERROR nodata answers are the result for that
> name.
> +    With a specific RR instead of a domain name, that specific record is
> +    removed from the local data, and not all the RR data.
>
>
>  @@UAHL@unbound-control.commands@local_zones@@
> Index: doc/unbound-host.1.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-host.1.in,v
> diff -u -p -r1.51 unbound-host.1.in
> --- doc/unbound-host.1.in       27 Jul 2026 14:14:39 -0000      1.51
> +++ doc/unbound-host.1.in       20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND-HOST" "1" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND-HOST" "1" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound-host \- Unbound 1.25.2 DNS lookup utility.
> +unbound-host \- Unbound 1.26.1 DNS lookup utility.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound\-host\fP [\fB\-C configfile\fP] [\fB\-vdhr46D\fP] [\fB\-c
> class\fP]
> Index: doc/unbound.8.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound.8.in,v
> diff -u -p -r1.52 unbound.8.in
> --- doc/unbound.8.in    27 Jul 2026 14:14:39 -0000      1.52
> +++ doc/unbound.8.in    20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND" "8" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND" "8" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound \- Unbound DNS validating resolver 1.25.2.
> +unbound \- Unbound DNS validating resolver 1.26.1.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound\fP [\fB\-hdpVv\fP] [\fB\-c <cfgfile>\fP]
> Index: doc/unbound.conf.5.in
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound.conf.5.in,v
> diff -u -p -r1.57 unbound.conf.5.in
> --- doc/unbound.conf.5.in       27 Jul 2026 14:14:39 -0000      1.57
> +++ doc/unbound.conf.5.in       20 Sep 2026 09:50:48 -0000
> @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2
>  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
>  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
>  ..
> -.TH "UNBOUND.CONF" "5" "Jul 22, 2026" "1.25.2" "Unbound"
> +.TH "UNBOUND.CONF" "5" "Sep 16, 2026" "1.26.1" "Unbound"
>  .SH NAME
> -unbound.conf \- Unbound 1.25.2 configuration file.
> +unbound.conf \- Unbound 1.26.1 configuration file.
>  .SH SYNOPSIS
>  .sp
>  \fBunbound.conf\fP
> @@ -636,7 +636,7 @@ Default: 0 (use system value)
>  .TP
>  .B so\-sndbuf: \fI<number>\fP
>  If not 0, then set the SO_SNDBUF socket option to get more buffer space on
> -UDP port 53 outgoing queries.
> +UDP port 53 outgoing responses.
>  This for very busy servers handles spikes in answer traffic, otherwise:
>  .INDENT 7.0
>  .INDENT 3.5
> @@ -2230,6 +2230,13 @@ The defensive action is to clear the rrs
>  flushing away any poison.
>  A value of 10 million is suggested.
>  .sp
> +It is useful to add 0.0.0.0/8 and \(aq::\(aq to the
> +\fI\%do\-not\-query\-address\fP list.
> +Otherwise they may be answered, from localhost, and the different source
> +makes an unwanted reply that unnecessarily ticks up.
> +The \fI\%do\-not\-query\-localhost\fP
> +option includes them, the zero subnets, when it is enabled.
> +.sp
>  Default: 0 (disabled)
>  .UNINDENT
>  .INDENT 0.0
> @@ -2280,6 +2287,8 @@ If yes, deny queries of type ANY with an
>  If disabled, Unbound responds with a short list of resource records if
> some
>  can be found in the cache and makes the upstream type ANY query if there
>  are none.
> +The option stops the DNSSEC validation from processing, possibly lengthy,
> +ANY responses, when the option is enabled.
>  .sp
>  Default: no
>  .UNINDENT
> @@ -2828,6 +2837,9 @@ The types are
>  \fI\%inform_redirect\fP,
>  \fI\%always_transparent\fP,
>  \fI\%block_a\fP,
> +\fI\%block_aaaa\fP,
> +\fI\%block_a_wdata\fP,
> +\fI\%block_aaaa_wdata\fP,
>  \fI\%always_refuse\fP,
>  \fI\%always_nxdomain\fP,
>  \fI\%always_null\fP,
> @@ -3018,6 +3030,32 @@ use IPv6 protocol and avoid any queries
>  .UNINDENT
>  .INDENT 7.0
>  .TP
> +.B block_aaaa
> +Like \fI\%transparent\fP or
> +\fI\%block_a\fP, but
> +ignores local data and resolves normally all query types excluding AAAA.
> +For AAAA queries it unconditionally returns NODATA.
> +Useful in cases when there is a need to explicitly force all apps to
> +use IPv4 protocol and avoid any queries to IPv6.
> +.UNINDENT
> +.INDENT 7.0
> +.TP
> +.B block_a_wdata
> +Like \fI\%block_a\fP, but
> +uses local data if present.
> +If there is local data that is returned, and it acts like transparent.
> +For A queries it returns NODATA.
> +.UNINDENT
> +.INDENT 7.0
> +.TP
> +.B block_aaaa_wdata
> +Like \fI\%block_aaaa\fP, but
> +uses local data if present.
> +If there is local data that is returned, and it acts like transparent.
> +For AAAA queries it returns NODATA.
> +.UNINDENT
> +.INDENT 7.0
> +.TP
>  .B always_refuse
>  Like \fI\%refuse\fP, but ignores
>  local data and refuses the query.
> @@ -3485,6 +3523,18 @@ For example, 1000 may be a suitable valu
>  overloaded with random names, and keeps unbound from sending traffic to
> the
>  nameservers for those zones.
>  .sp
> +It is intended to count the number of queries towards the nameservers
> +for the zone, and keep those queries limited.
> +When there is a delegation that needs a lot of lookups, those are
> +charged in the counters for the destination, the target name, of
> +the NS records.
> +Since that is where the nameserver lookup queries are sent to.
> +That keeps the target, the victim domain, from having many queries.
> +With the \fI\%ratelimit\-factor\fP, some
> +genuine queries that are also made to the target zone, can filter
> +through, and then end up in cache, where the genuine answers have
> +a chance to collect, keeping up service to some extent.
> +.sp
>  \fBNOTE:\fP
>  .INDENT 7.0
>  .INDENT 3.5
> @@ -4512,6 +4562,32 @@ If not given then no zonefile is used.
>  If the file does not exist or is empty, Unbound will attempt to fetch zone
>  data (eg. from the primary servers).
>  .UNINDENT
> +.INDENT 0.0
> +.TP
> +.B max\-transfer\-size: \fI<number>\fP
> +Number of bytes size of the maximum zone transfer size.
> +Larger transfers, over AXFR, IXFR and HTTP, are not allowed.
> +A plain number is in bytes, append \(aqk\(aq, \(aqm\(aq or \(aqg\(aq for
> kilobytes, megabytes
> +or gigabytes (1024*1024 bytes in a megabyte).
> +The value \fB0\fP disables the feature.
> +.sp
> +Only consider for untrusted/misbehaving primaries that could hog resources
> +and bring down the resolver.
> +.sp
> +Default: 0
> +.UNINDENT
> +.INDENT 0.0
> +.TP
> +.B max\-transfer\-time: \fI<msec>\fP
> +Maximum time in milliseconds that a zone transfer is allowed to take from
> +the start.
> +The value \fB0\fP disables the feature.
> +.sp
> +Only consider for untrusted/misbehaving primaries that could hog resources
> +and bring down the resolver.
> +.sp
> +Default: 0
> +.UNINDENT
>  .SH VIEW OPTIONS
>  .sp
>  These options are part of the \fBview:\fP section.
> @@ -5724,6 +5800,10 @@ from a webserver that would work.
>  If you specify the hostname, you cannot use the domain from the zonefile,
>  because it may not have that when retrieving that data, instead use a
> plain
>  IP address to avoid a circular dependency on retrieving that IP address.
> +.sp
> +Every number of IXFR transfers, a full AXFR is performed.
> +This is to consolidate the rpz memory, that would otherwise grow.
> +The fixed value is after 5 IXFR transfers.
>  .UNINDENT
>  .INDENT 0.0
>  .TP
> @@ -5845,6 +5925,32 @@ Enclose list of tags in quotes (\fB\(dq\
>  .sp
>  If no tags are specified the policies from this section will be applied
> for
>  all clients.
> +.UNINDENT
> +.INDENT 0.0
> +.TP
> +.B max\-transfer\-size: \fI<number>\fP
> +Number of bytes size of the maximum zone transfer size.
> +Larger transfers, over AXFR, IXFR and HTTP, are not allowed.
> +A plain number is in bytes, append \(aqk\(aq, \(aqm\(aq or \(aqg\(aq for
> kilobytes, megabytes
> +or gigabytes (1024*1024 bytes in a megabyte).
> +The value \fB0\fP disables the feature.
> +.sp
> +Only consider for untrusted/misbehaving primaries that could hog resources
> +and bring down the resolver.
> +.sp
> +Default: 0
> +.UNINDENT
> +.INDENT 0.0
> +.TP
> +.B max\-transfer\-time: \fI<msec>\fP
> +Maximum time in milliseconds that a zone transfer is allowed to take from
> +the start.
> +The value \fB0\fP disables the feature.
> +.sp
> +Only consider for untrusted/misbehaving primaries that could hog resources
> +and bring down the resolver.
> +.sp
> +Default: 0
>  .UNINDENT
>  .SH MEMORY CONTROL EXAMPLE
>  .sp
> Index: doc/unbound.conf.rst
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/doc/unbound.conf.rst,v
> diff -u -p -r1.6 unbound.conf.rst
> --- doc/unbound.conf.rst        27 Jul 2026 14:14:39 -0000      1.6
> +++ doc/unbound.conf.rst        20 Sep 2026 09:50:48 -0000
> @@ -642,7 +642,7 @@ These options are part of the ``server:`
>
>  @@UAHL@unbound.conf@so-sndbuf@@: *<number>*
>      If not 0, then set the SO_SNDBUF socket option to get more buffer
> space on
> -    UDP port 53 outgoing queries.
> +    UDP port 53 outgoing responses.
>      This for very busy servers handles spikes in answer traffic,
> otherwise:
>
>      .. code-block:: text
> @@ -2107,6 +2107,8 @@ These options are part of the ``server:`
>      If disabled, Unbound responds with a short list of resource records
> if some
>      can be found in the cache and makes the upstream type ANY query if
> there
>      are none.
> +    The option stops the DNSSEC validation from processing, possibly
> lengthy,
> +    ANY responses, when the option is enabled.
>
>      Default: no
>
> @@ -2318,7 +2320,7 @@ These options are part of the ``server:`
>      Use this setting to protect the users that rely on this validator for
>      authentication from potentially bad data in the additional section.
>
> -    Default: yes
> +    Default: no
>
>
>  @@UAHL@unbound.conf@val-log-level@@: *<number>*
> @@ -2590,6 +2592,9 @@ These options are part of the ``server:`
>      :ref:`inform_redirect<unbound.conf.local-zone.type.inform_redirect>`,
>
>  :ref:`always_transparent<unbound.conf.local-zone.type.always_transparent>`,
>      :ref:`block_a<unbound.conf.local-zone.type.block_a>`,
> +    :ref:`block_aaaa<unbound.conf.local-zone.type.block_aaaa>`,
> +    :ref:`block_a_wdata<unbound.conf.local-zone.type.block_a_wdata>`,
> +
> :ref:`block_aaaa_wdata<unbound.conf.local-zone.type.block_aaaa_wdata>`,
>      :ref:`always_refuse<unbound.conf.local-zone.type.always_refuse>`,
>      :ref:`always_nxdomain<unbound.conf.local-zone.type.always_nxdomain>`,
>      :ref:`always_null<unbound.conf.local-zone.type.always_null>`,
> @@ -2739,6 +2744,26 @@ These options are part of the ``server:`
>          Useful in cases when there is a need to explicitly force all apps
> to
>          use IPv6 protocol and avoid any queries to IPv4.
>
> +    @@UAHL@unbound.conf.local-zone.type@block_aaaa@@
> +        Like :ref:`transparent<unbound.conf.local-zone.type.transparent>`
> or
> +        :ref:`block_a<unbound.conf.local-zone.type.block_a>`, but
> +        ignores local data and resolves normally all query types
> excluding AAAA.
> +        For AAAA queries it unconditionally returns NODATA.
> +        Useful in cases when there is a need to explicitly force all apps
> to
> +        use IPv4 protocol and avoid any queries to IPv6.
> +
> +    @@UAHL@unbound.conf.local-zone.type@block_a_wdata@@
> +        Like :ref:`block_a<unbound.conf.local-zone.type.block_a>`, but
> +        uses local data if present.
> +        If there is local data that is returned, and it acts like
> transparent.
> +        For A queries it returns NODATA.
> +
> +    @@UAHL@unbound.conf.local-zone.type@block_aaaa_wdata@@
> +        Like :ref:`block_aaaa<unbound.conf.local-zone.type.block_aaaa>`,
> but
> +        uses local data if present.
> +        If there is local data that is returned, and it acts like
> transparent.
> +        For AAAA queries it returns NODATA.
> +
>      @@UAHL@unbound.conf.local-zone.type@always_refuse@@
>          Like :ref:`refuse<unbound.conf.local-zone.type.refuse>`, but
> ignores
>          local data and refuses the query.
> @@ -3085,6 +3110,18 @@ These options are part of the ``server:`
>      overloaded with random names, and keeps unbound from sending traffic
> to the
>      nameservers for those zones.
>
> +    It is intended to count the number of queries towards the nameservers
> +    for the zone, and keep those queries limited.
> +    When there is a delegation that needs a lot of lookups, those are
> +    charged in the counters for the destination, the target name, of
> +    the NS records.
> +    Since that is where the nameserver lookup queries are sent to.
> +    That keeps the target, the victim domain, from having many queries.
> +    With the :ref:`ratelimit-factor<unbound.conf.ratelimit-factor>`, some
> +    genuine queries that are also made to the target zone, can filter
> +    through, and then end up in cache, where the genuine answers have
> +    a chance to collect, keeping up service to some extent.
> +
>      .. note:: Configured forwarders are excluded from ratelimiting.
>
>      Default: 0
> @@ -3321,6 +3358,26 @@ These options are part of the ``server:`
>      Default: yes
>
>
> +@@UAHL@unbound.conf@val-validation-attempts@@: *<number>*
> +    Limit on the number of DNSSEC validation attempts for a query.
> +    This protects against too large numbers of cryptographic operations,
> +    like for a deep delegation chain.
> +    This counts attempts to validate RRSIGs.
> +    When it is exceeded, the query fails.
> +
> +    Default: 32
> +
> +
> +@@UAHL@unbound.conf@val-hash-attempts@@: *<number>*
> +    Limit on the number of DNSSEC hash attempts for a query.
> +    This protects against too large numbers of cryptographic operations,
> +    like for a deep delegation chain.
> +    This counts DS hash attempts to match DNSKEYs.
> +    When it is exceeded, the query fails.
> +
> +    Default: 32
> +
> +
>  @@UAHL@unbound.conf@fast-server-permil@@: *<number>*
>      Specify how many times out of 1000 to pick from the set of fastest
> servers.
>      0 turns the feature off.
> @@ -4018,6 +4075,31 @@ fallback activates to fetch from the ups
>      If the file does not exist or is empty, Unbound will attempt to fetch
> zone
>      data (eg. from the primary servers).
>
> +
> +@@UAHL@unbound.conf.auth@max-transfer-size@@: *<number>*
> +    Number of bytes size of the maximum zone transfer size.
> +    Larger transfers, over AXFR, IXFR and HTTP, are not allowed.
> +    A plain number is in bytes, append 'k', 'm' or 'g' for kilobytes,
> megabytes
> +    or gigabytes (1024*1024 bytes in a megabyte).
> +    The value ``0`` disables the feature.
> +
> +    Only consider for untrusted/misbehaving primaries that could hog
> resources
> +    and bring down the resolver.
> +
> +    Default: 0
> +
> +
> +@@UAHL@unbound.conf.auth@max-transfer-time@@: *<msec>*
> +    Maximum time in milliseconds that a zone transfer is allowed to take
> from
> +    the start.
> +    The value ``0`` disables the feature.
> +
> +    Only consider for untrusted/misbehaving primaries that could hog
> resources
> +    and bring down the resolver.
> +
> +    Default: 0
> +
> +
>  .. _unbound.conf.view:
>
>  View Options
> @@ -5098,6 +5180,10 @@ answer queries with that content.
>      because it may not have that when retrieving that data, instead use a
> plain
>      IP address to avoid a circular dependency on retrieving that IP
> address.
>
> +    Every number of IXFR transfers, a full AXFR is performed.
> +    This is to consolidate the rpz memory, that would otherwise grow.
> +    The fixed value is after 5 IXFR transfers.
> +
>
>  @@UAHL@unbound.conf.rpz@master@@: *<IP address or host name>*
>      Alternate syntax for :ref:`primary<unbound.conf.rpz.primary>`.
> @@ -5197,6 +5283,31 @@ answer queries with that content.
>
>      If no tags are specified the policies from this section will be
> applied for
>      all clients.
> +
> +
> +@@UAHL@unbound.conf.rpz@max-transfer-size@@: *<number>*
> +    Number of bytes size of the maximum zone transfer size.
> +    Larger transfers, over AXFR, IXFR and HTTP, are not allowed.
> +    A plain number is in bytes, append 'k', 'm' or 'g' for kilobytes,
> megabytes
> +    or gigabytes (1024*1024 bytes in a megabyte).
> +    The value ``0`` disables the feature.
> +
> +    Only consider for untrusted/misbehaving primaries that could hog
> resources
> +    and bring down the resolver.
> +
> +    Default: 0
> +
> +
> +@@UAHL@unbound.conf.rpz@max-transfer-time@@: *<msec>*
> +    Maximum time in milliseconds that a zone transfer is allowed to take
> from
> +    the start.
> +    The value ``0`` disables the feature.
> +
> +    Only consider for untrusted/misbehaving primaries that could hog
> resources
> +    and bring down the resolver.
> +
> +    Default: 0
> +
>
>  Memory Control Example
>  ----------------------
> Index: edns-subnet/addrtree.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/edns-subnet/addrtree.c,v
> diff -u -p -r1.4 addrtree.c
> --- edns-subnet/addrtree.c      20 Oct 2022 08:26:14 -0000      1.4
> +++ edns-subnet/addrtree.c      20 Sep 2026 09:50:48 -0000
> @@ -459,6 +459,7 @@ addrtree_insert(struct addrtree *tree, c
>                         /* Data is stored in other leafnode */
>                         node = newnode;
>                         newnode = node_create(tree, elem, scope, ttl);
> +                       if (!newnode) return;
>                         if (!edge_create(newnode, addr, sourcemask, node,
>                                 index^1)) {
>                                 clean_node(tree, newnode);
> Index: edns-subnet/subnetmod.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/edns-subnet/subnetmod.c,v
> diff -u -p -r1.20 subnetmod.c
> --- edns-subnet/subnetmod.c     26 May 2026 11:14:11 -0000      1.20
> +++ edns-subnet/subnetmod.c     20 Sep 2026 09:50:48 -0000
> @@ -1015,6 +1015,7 @@ subnetmod_operate(struct module_qstate *
>                         subnet_ecs_opt_list_append(&sq->ecs_client_out,
>                                 &qstate->edns_opts_front_out, qstate,
>                                 qstate->region);
> +                       qstate->is_subnet_answer = 1;
>                 }
>                 sq->wait_subquery_done = 0;
>                 qstate->ext_state[id] = module_finished;
> @@ -1094,6 +1095,7 @@ subnetmod_operate(struct module_qstate *
>                                 qstate->env->cfg->prefetch)) {
>                                 sne->num_msg_cache++;
>                                 lock_rw_unlock(&sne->biglock);
> +                               qstate->is_subnet_answer = 1;
>                                 verbose(VERB_QUERY, "subnetcache: answered
> from cache");
>                                 qstate->ext_state[id] = module_finished;
>
> @@ -1165,6 +1167,7 @@ subnetmod_operate(struct module_qstate *
>                         subnet_ecs_opt_list_append(&sq->ecs_client_out,
>                                 &qstate->edns_opts_front_out, qstate,
>                                 qstate->region);
> +                       qstate->is_subnet_answer = 1;
>                         if(verbosity >= VERB_ALGO) {
>                                 subnet_log_print("reply has edns subnet",
>                                         edns_opt_list_find(
> Index: ipsecmod/ipsecmod-whitelist.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/ipsecmod/ipsecmod-whitelist.c,v
> diff -u -p -r1.1 ipsecmod-whitelist.c
> --- ipsecmod/ipsecmod-whitelist.c       12 Aug 2017 11:22:46 -0000      1.1
> +++ ipsecmod/ipsecmod-whitelist.c       20 Sep 2026 09:50:48 -0000
> @@ -100,6 +100,8 @@ ipsecmod_whitelist_apply_cfg(struct ipse
>         struct config_file* cfg)
>  {
>         ie->whitelist = rbtree_create(name_tree_compare);
> +       if (!ie->whitelist)
> +               return 0;
>         if(!read_whitelist(ie->whitelist, cfg))
>                 return 0;
>         name_tree_init_parents(ie->whitelist);
> Index: ipsecmod/ipsecmod.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/ipsecmod/ipsecmod.c,v
> diff -u -p -r1.9 ipsecmod.c
> --- ipsecmod/ipsecmod.c 26 May 2026 11:14:11 -0000      1.9
> +++ ipsecmod/ipsecmod.c 20 Sep 2026 09:50:48 -0000
> @@ -51,6 +51,9 @@
>  #include "util/config_file.h"
>  #include "services/cache/dns.h"
>  #include "sldns/wire2str.h"
> +#ifdef HAVE_SYS_WAIT_H
> +#include <sys/wait.h>
> +#endif
>
>  /** Apply configuration to ipsecmod module 'global' state. */
>  static int
> @@ -60,6 +63,11 @@ ipsecmod_apply_cfg(struct ipsecmod_env*
>                 log_err("ipsecmod: missing ipsecmod-hook.");
>                 return 0;
>         }
> +       if(access(cfg->ipsecmod_hook, X_OK) != 0) {
> +               log_err("ipsecmod: ipsecmod-hook '%s' is not an executable
> file: %s",
> +                       cfg->ipsecmod_hook, strerror(errno));
> +               return 0;
> +       }
>         if(cfg->ipsecmod_whitelist &&
>                 !ipsecmod_whitelist_apply_cfg(ipsecmod_env, cfg))
>                 return 0;
> @@ -250,27 +258,16 @@ call_hook(struct module_qstate* qstate,
>         struct ipsecmod_env* ATTR_UNUSED(ie))
>  {
>         size_t slen, tempdata_len, tempstring_len, i;
> -       char str[65535], *s, *tempstring;
> +       char qname_s[LDNS_MAX_DOMAINLEN*5+16], ttl_s[32], a_s[32768],
> k_s[32768];
> +       char *s, *tempstring;
>         int w = 0, w_temp, qtype;
>         struct ub_packed_rrset_key* rrset_key;
>         struct packed_rrset_data* rrset_data;
>         uint8_t *tempdata;
> +       pid_t pid;
> +       int st;
> +       char* argv[6];
>
> -       /* Check if a shell is available */
> -       if(system(NULL) == 0) {
> -               log_err("ipsecmod: no shell available for ipsecmod-hook");
> -               return 0;
> -       }
> -
> -       /* Zero the buffer. */
> -       s = str;
> -       slen = sizeof(str);
> -       memset(s, 0, slen);
> -
> -       /* Copy the hook into the buffer. */
> -       w += sldns_str_print(&s, &slen, "%s",
> qstate->env->cfg->ipsecmod_hook);
> -       /* Put space into the buffer. */
> -       w += sldns_str_print(&s, &slen, " ");
>         /* Copy the qname into the buffer. */
>         tempstring = sldns_wire2str_dname(qstate->qinfo.qname,
>                 qstate->qinfo.qname_len);
> @@ -283,17 +280,24 @@ call_hook(struct module_qstate* qstate,
>                 free(tempstring);
>                 return 0;
>         }
> -       w += sldns_str_print(&s, &slen, "\"%s\"", tempstring);
> +       if(strlen(tempstring)+1 > sizeof(qname_s)) {
> +               log_err("ipsecmod: string too long");
> +               free(tempstring);
> +               return 0;
> +       }
> +       snprintf(qname_s, sizeof(qname_s), "%s", tempstring);
>         free(tempstring);
> -       /* Put space into the buffer. */
> -       w += sldns_str_print(&s, &slen, " ");
> +
>         /* Copy the IPSECKEY TTL into the buffer. */
>         rrset_data = (struct
> packed_rrset_data*)iq->ipseckey_rrset->entry.data;
> -       w += sldns_str_print(&s, &slen, "\"%ld\"", (long)rrset_data->ttl);
> -       /* Put space into the buffer. */
> -       w += sldns_str_print(&s, &slen, " ");
> +       snprintf(ttl_s, sizeof(ttl_s), "%ld", (long)rrset_data->ttl);
> +
>         rrset_key = reply_find_answer_rrset(&qstate->return_msg->qinfo,
>                 qstate->return_msg->rep);
> +       if(!rrset_key) {
> +               log_err("ipsecmod: could not find answer rrset for
> A/AAAA");
> +               return 0;
> +       }
>         /* Double check that the records are indeed A/AAAA.
>          * This should never happen as this function is only executed for
> A/AAAA
>          * queries but make sure we don't pass anything other than A/AAAA
> to the
> @@ -304,9 +308,15 @@ call_hook(struct module_qstate* qstate,
>                 return 0;
>         }
>         rrset_data = (struct packed_rrset_data*)rrset_key->entry.data;
> -       /* Copy the A/AAAA record(s) into the buffer. Start and end this
> section
> -        * with a double quote. */
> -       w += sldns_str_print(&s, &slen, "\"");
> +       if(!rrset_data) {
> +               log_err("ipsecmod: Answer has no data");
> +               return 0;
> +       }
> +       /* Copy the A/AAAA record(s) into the buffer. */
> +       w = 0;
> +       s = a_s;
> +       slen = sizeof(a_s);
> +       memset(s, 0, slen);
>         for(i=0; i<rrset_data->count; i++) {
>                 if(i > 0) {
>                         /* Put space into the buffer. */
> @@ -322,7 +332,7 @@ call_hook(struct module_qstate* qstate,
>                 } else if((size_t)w_temp >= slen) {
>                         s = NULL; /* We do not want str to point outside
> of buffer. */
>                         slen = 0;
> -                       log_err("ipsecmod: shell command too long");
> +                       log_err("ipsecmod: command addr argument too
> long");
>                         return 0;
>                 } else {
>                         s += w_temp;
> @@ -330,12 +340,17 @@ call_hook(struct module_qstate* qstate,
>                         w += w_temp;
>                 }
>         }
> -       w += sldns_str_print(&s, &slen, "\"");
> -       /* Put space into the buffer. */
> -       w += sldns_str_print(&s, &slen, " ");
> +       if(w >= (int)sizeof(a_s)) {
> +               log_err("ipsecmod: command addr argument too long");
> +               return 0;
> +       }
> +
>         /* Copy the IPSECKEY record(s) into the buffer. Start and end this
> section
>          * with a double quote. */
> -       w += sldns_str_print(&s, &slen, "\"");
> +       w = 0;
> +       s = k_s;
> +       slen = sizeof(k_s);
> +       memset(s, 0, slen);
>         rrset_data = (struct
> packed_rrset_data*)iq->ipseckey_rrset->entry.data;
>         for(i=0; i<rrset_data->count; i++) {
>                 if(i > 0) {
> @@ -362,15 +377,44 @@ call_hook(struct module_qstate* qstate,
>                         w += w_temp;
>                 }
>         }
> -       w += sldns_str_print(&s, &slen, "\"");
> -       if(w >= (int)sizeof(str)) {
> -               log_err("ipsecmod: shell command too long");
> +       if(w >= (int)sizeof(k_s)) {
> +               log_err("ipsecmod: command ipseckey argument too long");
>                 return 0;
>         }
> -       verbose(VERB_ALGO, "ipsecmod: shell command: '%s'", str);
> +
>         /* ipsecmod-hook should return 0 on success. */
> -       if(system(str) != 0)
> +       /* exec the ipsecmod-hook */
> +       argv[0] = qstate->env->cfg->ipsecmod_hook;
> +       argv[1] = qname_s;
> +       argv[2] = ttl_s;
> +       argv[3] = a_s;
> +       argv[4] = k_s;
> +       argv[5] = NULL;
> +       verbose(VERB_ALGO, "ipsecmod: exec %s \"%s\" %s \"%s\" \"%s\"",
> +               argv[0], argv[1], argv[2], argv[3], argv[4]);
> +       if((pid = fork()) < 0) {
> +               log_err("ipsecmod: for exec, can not fork: %s",
> +                       strerror(errno));
> +               return 0;
> +       }
> +       if(pid == 0) {
> +               if(execv(argv[0], argv) < 0)
> +                       fprintf(stderr, "ipsecmod: execv: %s\n",
> +                               strerror(errno));
> +               _exit(127);
> +       }
> +       while(1) {
> +               if(waitpid(pid, &st, 0) < 0) {
> +                       if(errno == EINTR)
> +                               continue;
> +                       log_err("ipsecmod: wait_pid: %s", strerror(errno));
> +               }
> +               break;
> +       }
> +       if(!(WIFEXITED(st) && WEXITSTATUS(st) == 0)) {
> +               /* the command failed */
>                 return 0;
> +       }
>         return 1;
>  }
>
> @@ -435,6 +479,12 @@ ipsecmod_handle_query(struct module_qsta
>                          * ipsecmod_max_ttl. */
>                         rrset_key =
> reply_find_answer_rrset(&qstate->return_msg->qinfo,
>                                 qstate->return_msg->rep);
> +                       if(!rrset_key) {
> +                               log_err("ipsecmod: reply-find-answer
> failed");
> +                               errinf(qstate, "ipsecmod:
> reply-find-answer failed");
> +                               ipsecmod_error(qstate, id);
> +                               return;
> +                       }
>                         rrset_data = (struct
> packed_rrset_data*)rrset_key->entry.data;
>                         if(rrset_data->ttl >
> (time_t)qstate->env->cfg->ipsecmod_max_ttl) {
>                                 /* Update TTL for rrset to fixed value. */
> Index: ipset/ipset.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/ipset/ipset.c,v
> diff -u -p -r1.2 ipset.c
> --- ipset/ipset.c       4 Sep 2024 09:36:40 -0000       1.2
> +++ ipset/ipset.c       20 Sep 2026 09:50:48 -0000
> @@ -129,7 +129,7 @@ static int add_to_ipset(filter_dev dev,
>                 default:
>                 errno = EAFNOSUPPORT;
>                 return -1;
> -}
> +       }
>         addr.pfra_af = af;
>
>         if (ioctl(dev, DIOCRADDADDRS, &io) == -1) {
> @@ -143,7 +143,7 @@ static int add_to_ipset(filter_dev dev,
>         struct nlmsghdr *nlh;
>         struct nfgenmsg *nfg;
>         struct nlattr *nested[2];
> -       static char buffer[BUFF_LEN];
> +       char buffer[BUFF_LEN];
>
>         if (strlen(setname) >= IPSET_MAXNAMELEN) {
>                 errno = ENAMETOOLONG;
> @@ -208,13 +208,6 @@ ipset_add_rrset_data(struct ipset_env *i
>                         ret = add_to_ipset((filter_dev)ie->dev, setname,
> rr_data + 2, af);
>                         if (ret < 0) {
>                                 log_err("ipset: could not add %s into %s",
> dname, setname);
> -
> -#if HAVE_NET_PFVAR_H
> -                               /* don't close as we might not be able to
> open again due to dropped privs */
> -#else
> -                               mnl_socket_close((filter_dev)ie->dev);
> -                               ie->dev = NULL;
> -#endif
>                                 break;
>                         }
>                 }
> @@ -226,15 +219,15 @@ ipset_check_zones_for_rrset(struct modul
>         struct ub_packed_rrset_key *rrset, const char *qname, int qlen,
>         const char *setname, int af)
>  {
> -       static char dname[BUFF_LEN];
> +       char dname[LDNS_MAX_DOMAINLEN*4+16];
>         const char *ds, *qs;
>         int dlen, plen;
>
>         struct config_strlist *p;
>         struct packed_rrset_data *d;
>
> -       dlen = sldns_wire2str_dname_buf(rrset->rk.dname,
> rrset->rk.dname_len, dname, BUFF_LEN);
> -       if (dlen == 0) {
> +       dlen = sldns_wire2str_dname_buf(rrset->rk.dname,
> rrset->rk.dname_len, dname, sizeof(dname));
> +       if (dlen == 0 || dlen >= (int)sizeof(dname)) {
>                 log_err("bad domain name");
>                 return -1;
>         }
> @@ -276,7 +269,7 @@ static int ipset_update(struct module_en
>         const char *setname;
>         struct ub_packed_rrset_key *rrset;
>         int af;
> -       static char qname[BUFF_LEN];
> +       char qname[LDNS_MAX_DOMAINLEN*4+16];
>         int qlen;
>
>  #ifdef HAVE_NET_PFVAR_H
> @@ -292,8 +285,8 @@ static int ipset_update(struct module_en
>  #endif
>
>         qlen = sldns_wire2str_dname_buf(qinfo.qname, qinfo.qname_len,
> -               qname, BUFF_LEN);
> -       if(qlen == 0) {
> +               qname, sizeof(qname));
> +       if(qlen == 0 || qlen >= (int)sizeof(qname)) {
>                 log_err("bad domain name");
>                 return -1;
>         }
> @@ -351,7 +344,7 @@ void ipset_destartup(struct module_env*
>         if (!env || !env->modinfo[id]) {
>                 return;
>         }
> -       ipset_env = (struct ipset_env *)env->modinfo[id];
> +       ipset_env = (struct ipset_env*)env->modinfo[id];
>
>         dev = (filter_dev)ipset_env->dev;
>         if (dev) {
> @@ -372,6 +365,16 @@ int ipset_init(struct module_env* env, i
>
>         ipset_env->name_v4 = env->cfg->ipset_name_v4;
>         ipset_env->name_v6 = env->cfg->ipset_name_v6;
> +#ifndef HAVE_NET_PFVAR_H
> +       if (ipset_env->name_v4 && strlen(ipset_env->name_v4) >=
> IPSET_MAXNAMELEN) {
> +               log_err("ipset: name-v4 exceeds IPSET_MAXNAMELEN (%d)",
> IPSET_MAXNAMELEN);
> +               return 0;
> +       }
> +       if (ipset_env->name_v6 && strlen(ipset_env->name_v6) >=
> IPSET_MAXNAMELEN) {
> +               log_err("ipset: name-v6 exceeds IPSET_MAXNAMELEN (%d)",
> IPSET_MAXNAMELEN);
> +               return 0;
> +       }
> +#endif
>
>         ipset_env->v4_enabled = !ipset_env->name_v4 ||
> (strlen(ipset_env->name_v4) == 0) ? 0 : 1;
>         ipset_env->v6_enabled = !ipset_env->name_v6 ||
> (strlen(ipset_env->name_v6) == 0) ? 0 : 1;
> Index: iterator/iter_delegpt.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_delegpt.c,v
> diff -u -p -r1.9 iter_delegpt.c
> --- iterator/iter_delegpt.c     31 Aug 2025 21:41:09 -0000      1.9
> +++ iterator/iter_delegpt.c     20 Sep 2026 09:50:48 -0000
> @@ -118,10 +118,10 @@ delegpt_add_ns(struct delegpt* dp, struc
>                 sizeof(struct delegpt_ns));
>         if(!ns)
>                 return 0;
> -       ns->next = dp->nslist;
>         ns->namelen = len;
> -       dp->nslist = ns;
>         ns->name = regional_alloc_init(region, name, ns->namelen);
> +       if(!ns->name)
> +               return 0;
>         ns->cache_lookup_count = 0;
>         ns->resolved = 0;
>         ns->got4 = 0;
> @@ -137,7 +137,9 @@ delegpt_add_ns(struct delegpt* dp, struc
>         } else {
>                 ns->tls_auth_name = NULL;
>         }
> -       return ns->name != 0;
> +       ns->next = dp->nslist;
> +       dp->nslist = ns;
> +       return 1;
>  }
>
>  struct delegpt_ns*
> @@ -223,11 +225,7 @@ delegpt_add_addr(struct delegpt* dp, str
>                 sizeof(struct delegpt_addr));
>         if(!a)
>                 return 0;
> -       a->next_target = dp->target_list;
> -       dp->target_list = a;
>         a->next_result = 0;
> -       a->next_usable = dp->usable_list;
> -       dp->usable_list = a;
>         memcpy(&a->addr, addr, addrlen);
>         a->addrlen = addrlen;
>         a->attempts = 0;
> @@ -241,6 +239,10 @@ delegpt_add_addr(struct delegpt* dp, str
>         } else {
>                 a->tls_auth_name = NULL;
>         }
> +       a->next_target = dp->target_list;
> +       dp->target_list = a;
> +       a->next_usable = dp->usable_list;
> +       dp->usable_list = a;
>         return 1;
>  }
>
> @@ -398,30 +400,33 @@ delegpt_count_missing_targets(struct del
>
>  /** find NS rrset in given list */
>  static struct ub_packed_rrset_key*
> -find_NS(struct reply_info* rep, size_t from, size_t to)
> +find_NS(struct reply_info* rep, size_t from, size_t to, uint16_t qclass)
>  {
>         size_t i;
>         for(i=from; i<to; i++) {
> -               if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NS)
> +               if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NS &&
> +                       ntohs(rep->rrsets[i]->rk.rrset_class) == qclass)
>                         return rep->rrsets[i];
>         }
>         return NULL;
>  }
>
>  struct delegpt*
> -delegpt_from_message(struct dns_msg* msg, struct regional* region)
> +delegpt_from_message(struct dns_msg* msg, struct regional* region, int
> port)
>  {
>         struct ub_packed_rrset_key* ns_rrset = NULL;
>         struct delegpt* dp;
>         size_t i;
>         /* look for NS records in the authority section... */
>         ns_rrset = find_NS(msg->rep, msg->rep->an_numrrsets,
> -               msg->rep->an_numrrsets+msg->rep->ns_numrrsets);
> +               msg->rep->an_numrrsets+msg->rep->ns_numrrsets,
> +               msg->qinfo.qclass);
>
>         /* In some cases (even legitimate, perfectly legal cases), the
>          * NS set for the "referral" might be in the answer section. */
>         if(!ns_rrset)
> -               ns_rrset = find_NS(msg->rep, 0, msg->rep->an_numrrsets);
> +               ns_rrset = find_NS(msg->rep, 0, msg->rep->an_numrrsets,
> +                       msg->qinfo.qclass);
>
>         /* If there was no NS rrset in the authority section, then this
>          * wasn't a referral message. (It might not actually be a
> @@ -436,7 +441,7 @@ delegpt_from_message(struct dns_msg* msg
>         dp->has_parent_side_NS = 1; /* created from message */
>         if(!delegpt_set_name(dp, region, ns_rrset->rk.dname))
>                 return NULL;
> -       if(!delegpt_rrset_add_ns(dp, region, ns_rrset, 0))
> +       if(!delegpt_rrset_add_ns(dp, region, ns_rrset, 0, port))
>                 return NULL;
>
>         /* add glue, A and AAAA in answer and additional section */
> @@ -447,10 +452,12 @@ delegpt_from_message(struct dns_msg* msg
>                         i <
> (msg->rep->an_numrrsets+msg->rep->ns_numrrsets))
>                         continue;
>
> -               if(ntohs(s->rk.type) == LDNS_RR_TYPE_A) {
> +               if(ntohs(s->rk.type) == LDNS_RR_TYPE_A &&
> +                       ntohs(s->rk.rrset_class) == msg->qinfo.qclass) {
>                         if(!delegpt_add_rrset_A(dp, region, s, 0, NULL))
>                                 return NULL;
> -               } else if(ntohs(s->rk.type) == LDNS_RR_TYPE_AAAA) {
> +               } else if(ntohs(s->rk.type) == LDNS_RR_TYPE_AAAA &&
> +                       ntohs(s->rk.rrset_class) == msg->qinfo.qclass) {
>                         if(!delegpt_add_rrset_AAAA(dp, region, s, 0, NULL))
>                                 return NULL;
>                 }
> @@ -460,7 +467,7 @@ delegpt_from_message(struct dns_msg* msg
>
>  int
>  delegpt_rrset_add_ns(struct delegpt* dp, struct regional* region,
> -        struct ub_packed_rrset_key* ns_rrset, uint8_t lame)
> +        struct ub_packed_rrset_key* ns_rrset, uint8_t lame, int port)
>  {
>         struct packed_rrset_data* nsdata = (struct packed_rrset_data*)
>                 ns_rrset->entry.data;
> @@ -475,7 +482,7 @@ delegpt_rrset_add_ns(struct delegpt* dp,
>                         continue; /* bad format */
>                 /* add rdata of NS (= wirefmt dname), skip rdatalen bytes
> */
>                 if(!delegpt_add_ns(dp, region, nsdata->rr_data[i]+2, lame,
> -                       NULL, UNBOUND_DNS_PORT))
> +                       NULL, (port==-1?UNBOUND_DNS_PORT:port)))
>                         return 0;
>         }
>         return 1;
> @@ -534,7 +541,7 @@ delegpt_add_rrset(struct delegpt* dp, st
>         if(!rrset)
>                 return 1;
>         if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_NS)
> -               return delegpt_rrset_add_ns(dp, region, rrset, lame);
> +               return delegpt_rrset_add_ns(dp, region, rrset, lame, -1);
>         else if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_A)
>                 return delegpt_add_rrset_A(dp, region, rrset, lame,
> additions);
>         else if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_AAAA)
> @@ -659,8 +666,6 @@ int delegpt_add_ns_mlc(struct delegpt* d
>                 free(ns);
>                 return 0;
>         }
> -       ns->next = dp->nslist;
> -       dp->nslist = ns;
>         ns->cache_lookup_count = 0;
>         ns->resolved = 0;
>         ns->got4 = 0;
> @@ -679,6 +684,8 @@ int delegpt_add_ns_mlc(struct delegpt* d
>         } else {
>                 ns->tls_auth_name = NULL;
>         }
> +       ns->next = dp->nslist;
> +       dp->nslist = ns;
>         return 1;
>  }
>
> @@ -704,11 +711,7 @@ int delegpt_add_addr_mlc(struct delegpt*
>         a = (struct delegpt_addr*)malloc(sizeof(struct delegpt_addr));
>         if(!a)
>                 return 0;
> -       a->next_target = dp->target_list;
> -       dp->target_list = a;
>         a->next_result = 0;
> -       a->next_usable = dp->usable_list;
> -       dp->usable_list = a;
>         memcpy(&a->addr, addr, addrlen);
>         a->addrlen = addrlen;
>         a->attempts = 0;
> @@ -724,6 +727,10 @@ int delegpt_add_addr_mlc(struct delegpt*
>         } else {
>                 a->tls_auth_name = NULL;
>         }
> +       a->next_target = dp->target_list;
> +       dp->target_list = a;
> +       a->next_usable = dp->usable_list;
> +       dp->usable_list = a;
>         return 1;
>  }
>
> Index: iterator/iter_delegpt.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_delegpt.h,v
> diff -u -p -r1.12 iter_delegpt.h
> --- iterator/iter_delegpt.h     26 Sep 2025 07:32:37 -0000      1.12
> +++ iterator/iter_delegpt.h     20 Sep 2026 09:50:48 -0000
> @@ -221,10 +221,11 @@ int delegpt_add_ns(struct delegpt* dp, s
>   * @param regional: where to allocate the info.
>   * @param ns_rrset: NS rrset.
>   * @param lame: rrset is lame, disprefer it.
> + * @param port: port or -1 if not set.
>   * @return 0 on alloc error.
>   */
>  int delegpt_rrset_add_ns(struct delegpt* dp, struct regional* regional,
> -       struct ub_packed_rrset_key* ns_rrset, uint8_t lame);
> +       struct ub_packed_rrset_key* ns_rrset, uint8_t lame, int port);
>
>  /**
>   * Add target address to the delegation point.
> @@ -365,11 +366,12 @@ size_t delegpt_count_targets(struct dele
>   *
>   * @param msg: the dns message, referral.
>   * @param regional: where to allocate delegation point.
> + * @param port: if not -1 specifies a port number.
>   * @return new delegation point or NULL on alloc error, or if the
>   *         message was not appropriate.
>   */
>  struct delegpt* delegpt_from_message(struct dns_msg* msg,
> -       struct regional* regional);
> +       struct regional* regional, int port);
>
>  /**
>   * Mark negative return in delegation point for specific nameserver.
> Index: iterator/iter_resptype.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_resptype.c,v
> diff -u -p -r1.7 iter_resptype.c
> --- iterator/iter_resptype.c    12 Apr 2024 15:45:24 -0000      1.7
> +++ iterator/iter_resptype.c    20 Sep 2026 09:50:48 -0000
> @@ -107,7 +107,7 @@ response_type_from_cache(struct dns_msg*
>  enum response_type
>  response_type_from_server(int rdset,
>         struct dns_msg* msg, struct query_info* request, struct delegpt*
> dp,
> -       int* empty_nodata_found)
> +       int* empty_nodata_found, int msg_lame_empty, int msg_lame_referral)
>  {
>         uint8_t* origzone = (uint8_t*)"\000"; /* the default */
>         struct ub_packed_rrset_key* s;
> @@ -122,6 +122,10 @@ response_type_from_server(int rdset,
>
>         /* If the message is NXDOMAIN, then it answers the question. */
>         if(FLAGS_GET_RCODE(msg->rep->flags) == LDNS_RCODE_NXDOMAIN) {
> +               if(msg->rep->an_numrrsets == 0 &&
> +                       msg->rep->ns_numrrsets == 0 &&
> +                       msg_lame_empty)
> +                       return RESPONSE_TYPE_LAME;
>                 /* make sure its not recursive when we don't want it to */
>                 if( (msg->rep->flags&BIT_RA) &&
>                         !(msg->rep->flags&BIT_AA) && !rdset)
> @@ -143,6 +147,10 @@ response_type_from_server(int rdset,
>         if(FLAGS_GET_RCODE(msg->rep->flags) != LDNS_RCODE_NOERROR)
>                 return RESPONSE_TYPE_THROWAWAY;
>
> +       if(msg->rep->an_numrrsets == 0 && msg->rep->ns_numrrsets == 0 &&
> +               msg_lame_empty)
> +               return RESPONSE_TYPE_LAME;
> +
>         /* Note: TC bit has already been handled */
>
>         if(dp) {
> @@ -249,13 +257,16 @@ response_type_from_server(int rdset,
>                                  * which gives ns==zone delegation from
> cache
>                                  * without AA bit as well, with nodata
> nosoa*/
>                                 /* real answer must be +AA and SOA
> RFC(2308),
> -                                * so this is wrong, and we SERVFAIL it if
> -                                * this is the only possible reply, if it
> -                                * is misdeployed the THROWAWAY makes us
> pick
> -                                * the next server from the selection */
> -                               if(msg->rep->an_numrrsets==0 &&
> +                                * this is picked up as lame_referral by
> the
> +                                * sanitize step, so it can spot if there
> +                                * was data in the answer section before
> +                                * removal. If such data is then removed we
> +                                * do not want to turn that answer into
> lame.
> +                                * But if it was not there, it can be
> lame. */
> +                               if(msg_lame_referral &&
> +                                       msg->rep->an_numrrsets==0 &&
>                                         !(msg->rep->flags&BIT_AA) &&
> !rdset)
> -                                       return RESPONSE_TYPE_THROWAWAY;
> +                                       return RESPONSE_TYPE_LAME;
>                                 return RESPONSE_TYPE_ANSWER;
>                         }
>                         /* If we are getting a referral upwards (or to
> Index: iterator/iter_resptype.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_resptype.h,v
> diff -u -p -r1.1.1.3 iter_resptype.h
> --- iterator/iter_resptype.h    12 Apr 2024 15:44:27 -0000      1.1.1.3
> +++ iterator/iter_resptype.h    20 Sep 2026 09:50:48 -0000
> @@ -120,10 +120,14 @@ enum response_type response_type_from_ca
>   * @param dp: The delegation point that was being queried
>   *          when the response was returned.
>   * @param empty_nodata_found: flag to keep track of empty nodata
> detection.
> + * @param msg_lame_empty: The scrubber indicates that this empty message
> + *     is lame, before it became empty.
> + * @param msg_lame_referral: returned true if the reply has a referral
> before
> + *     scrub.
>   * @return the response type (CNAME or ANSWER).
>   */
>  enum response_type response_type_from_server(int rdset,
>         struct dns_msg* msg, struct query_info* request, struct delegpt*
> dp,
> -       int* empty_nodata_found);
> +       int* empty_nodata_found, int msg_lame_empty, int
> msg_lame_referral);
>
>  #endif /* ITERATOR_ITER_RESPTYPE_H */
> Index: iterator/iter_scrub.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_scrub.c,v
> diff -u -p -r1.21 iter_scrub.c
> --- iterator/iter_scrub.c       26 May 2026 11:14:11 -0000      1.21
> +++ iterator/iter_scrub.c       20 Sep 2026 09:50:48 -0000
> @@ -294,7 +294,14 @@ synth_cname_rrset(uint8_t** sname, size_
>                 if(ttl_t > MAX_TTL) ttl_t = MAX_TTL;
>                 ttl = (uint32_t)ttl_t;
>                 sldns_write_uint32(cn->rr_first->ttl_data, ttl);
> -               sldns_write_uint32(rrset->rr_first->ttl_data, ttl);
> +               /* Do NOT write the clamp back into the packet buffer:
> +                * parse_packet already sized every name from the original
> +                * bytes and rdata_copy re-walks them trusting those sizes;
> +                * mutating packet bytes between the walks breaks that
> +                * invariant (compression pointers can target these TTL
> +                * bytes). The DNAME rrset receives the same clamp at store
> +                * time in rdata_copy, so the DNAME and the synthesized
> +                * CNAME still carry equal TTLs in the cache. */
>         }
>         sldns_write_uint16(cn->rr_first->ttl_data+4, aliaslen);
>         memmove(cn->rr_first->ttl_data+6, alias, aliaslen);
> @@ -316,6 +323,20 @@ synth_cname_rrset(uint8_t** sname, size_
>         return cn;
>  }
>
> +/** Check if the packet has type NS in answer or authority section */
> +static int
> +pkt_contains_ns(struct msg_parse* msg)
> +{
> +       struct rrset_parse* rrset;
> +       for(rrset = msg->rrset_first; rrset; rrset =
> rrset->rrset_all_next) {
> +               if(rrset->type == LDNS_RR_TYPE_NS &&
> +                       (rrset->section == LDNS_SECTION_ANSWER ||
> +                       rrset->section == LDNS_SECTION_AUTHORITY))
> +                       return 1;
> +       }
> +       return 0;
> +}
> +
>  /** check if DNAME applies to a name */
>  static int
>  pkt_strict_sub(sldns_buffer* pkt, uint8_t* sname, uint8_t* dr)
> @@ -394,6 +415,8 @@ shorten_rrset(sldns_buffer* pkt, struct
>         struct rr_parse* rr = rrset->rr_first, *prev = NULL;
>         if(!rr)
>                 return;
> +       if(count < 1)
> +               return; /* cannot leave a still-linked rrset_parse with
> rr_count == 0 */
>         for(i=0; i<count; i++) {
>                 prev = rr;
>                 rr = rr->next;
> @@ -478,6 +501,7 @@ scrub_normalize(sldns_buffer* pkt, struc
>         size_t snamelen = qinfo->qname_len;
>         struct rrset_parse* rrset, *prev, *nsset=NULL;
>         int cname_length = 0; /* number of CNAMEs, or DNAMEs */
> +       int has_answer = 0; /* if answer section contains
> nonCNAME,nonDNAME */
>
>         if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR &&
>                 FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN &&
> @@ -519,6 +543,11 @@ scrub_normalize(sldns_buffer* pkt, struc
>                                         (unsigned)rrset->rr_count);
>                                 return 0;
>                         }
> +                       if(has_answer) {
> +                               remove_rrset("normalize: removing DNAME
> redirection after answer:",
> +                                       pkt, msg, prev, &rrset);
> +                               continue;
> +                       }
>                         if(!synth_cname(sname, snamelen, rrset, alias,
>                                 &aliaslen, pkt)) {
>                                 verbose(VERB_ALGO, "synthesized CNAME "
> @@ -569,6 +598,11 @@ scrub_normalize(sldns_buffer* pkt, struc
>                 if(rrset->type == LDNS_RR_TYPE_CNAME) {
>                         struct rrset_parse* nx = rrset->rrset_all_next;
>                         uint8_t* oldsname = sname;
> +                       if(has_answer) {
> +                               remove_rrset("normalize: removing
> redirection after answer:",
> +                                       pkt, msg, prev, &rrset);
> +                               continue;
> +                       }
>                         cname_length++;
>                         /* see if the next one is a DNAME, if so, swap
> them */
>                         if(nx && nx->section == LDNS_SECTION_ANSWER &&
> @@ -621,6 +655,9 @@ scrub_normalize(sldns_buffer* pkt, struc
>                                         if(rrset->type == LDNS_RR_TYPE_NS
> &&
>                                                 rrset->rr_count >
> env->cfg->iter_scrub_ns) {
>                                                 shorten_rrset(pkt, rrset,
> env->cfg->iter_scrub_ns);
> +                                       } else if(rrset->type ==
> LDNS_RR_TYPE_DS &&
> +                                               rrset->rr_count >
> env->cfg->iter_scrub_ns) {
> +                                               shorten_rrset(pkt, rrset,
> env->cfg->iter_scrub_ns);
>                                         }
>                                         prev = rrset;
>                                         rrset = rrset->rrset_all_next;
> @@ -640,6 +677,9 @@ scrub_normalize(sldns_buffer* pkt, struc
>                 if(rrset->type == LDNS_RR_TYPE_NS &&
>                         rrset->rr_count > env->cfg->iter_scrub_ns) {
>                         shorten_rrset(pkt, rrset, env->cfg->iter_scrub_ns);
> +               } else if(rrset->type == LDNS_RR_TYPE_DS &&
> +                       rrset->rr_count > env->cfg->iter_scrub_ns) {
> +                       shorten_rrset(pkt, rrset, env->cfg->iter_scrub_ns);
>                 }
>
>                 /* Mark the additional names from relevant rrset as OK. */
> @@ -647,6 +687,7 @@ scrub_normalize(sldns_buffer* pkt, struc
>                  * will be removed by sanitize, so no additional for them
> */
>                 if(dname_pkt_compare(pkt, qinfo->qname, rrset->dname) == 0)
>                         mark_additional_rrset(pkt, msg, rrset);
> +               has_answer = 1;
>
>                 prev = rrset;
>                 rrset = rrset->rrset_all_next;
> @@ -732,6 +773,11 @@ scrub_normalize(sldns_buffer* pkt, struc
>                                         "RRset:", pkt, msg, prev, &rrset);
>                                 continue;
>                         }
> +                       if(ntohs(rrset->rrset_class) != qinfo->qclass) {
> +                               remove_rrset("normalize: removing other
> class "
> +                                       "RRset:", pkt, msg, prev, &rrset);
> +                               continue;
> +                       }
>                         if(nsset == NULL) {
>                                 nsset = rrset;
>                         } else {
> @@ -758,6 +804,11 @@ scrub_normalize(sldns_buffer* pkt, struc
>                                         shorten_rrset(pkt, rrset,
> env->cfg->iter_scrub_ns);
>                                 }
>                         }
> +               } else if(rrset->type==LDNS_RR_TYPE_DS) {
> +                       if(rrset->rr_count > env->cfg->iter_scrub_ns) {
> +                               shorten_rrset(pkt, rrset,
> +                                       env->cfg->iter_scrub_ns);
> +                       }
>                 }
>                 /* if this is type DS and we query for type DS we just got
>                  * a referral answer for our type DS query, fix packet */
> @@ -968,12 +1019,20 @@ scrub_sanitize_rr_length(sldns_buffer* p
>   * @param env: module environment with config and cache.
>   * @param ie: iterator environment with private address data.
>   * @param qstate: for setting errinf for EDE error messages.
> + * @param pkt_before_NS: if the packet had type NS before scrub. If that
> + *     is removed now, that indicates this may have been lame.
> + * @param msg_lame_empty: returned true if the empty packet is lame.
> + * @param msg_lame_referral: returned true if the reply has a referral
> before
> + *     scrub.
> + * @param rdset: if RD bit was sent in query sent by unbound.
>   * @return 0 on error.
>   */
>  static int
>  scrub_sanitize(sldns_buffer* pkt, struct msg_parse* msg,
>         struct query_info* qinfo, uint8_t* zonename, struct module_env*
> env,
> -       struct iter_env* ie, struct module_qstate* qstate)
> +       struct iter_env* ie, struct module_qstate* qstate,
> +       int pkt_before_NS, int* msg_lame_empty, int* msg_lame_referral,
> +       int rdset)
>  {
>         int del_addi = 0; /* if additional-holding rrsets are deleted, we
>                 do not trust the normalized additional-A-AAAA any more */
> @@ -1130,6 +1189,21 @@ scrub_sanitize(sldns_buffer* pkt, struct
>                 prev = rrset;
>                 rrset = rrset->rrset_all_next;
>         }
> +
> +       /* If the packet is empty now, but it was not before. And there
> +        * was type NS in authority, then that indicates the answer is
> lame. */
> +       if(msg->rrset_first == NULL && pkt_before_NS) {
> +               *msg_lame_empty = 1;
> +               verbose(VERB_ALGO, "sanitize: empty message had referral
> to NS before, marked as lame");
> +       } else if(pkt_before_NS && msg->an_rrsets==0 &&
> +               !(msg->flags&BIT_AA) && !rdset) {
> +               /* If the packet is now a referral, not really a nodata,
> +                * then if it was also with an empty answer section before,
> +                * it is also lame. */
> +               *msg_lame_referral = 1;
> +               verbose(VERB_ALGO, "sanitize: message has referral not
> answer, marked as lame");
> +       }
> +
>         return 1;
>  }
>
> @@ -1137,11 +1211,15 @@ int
>  scrub_message(sldns_buffer* pkt, struct msg_parse* msg,
>         struct query_info* qinfo, uint8_t* zonename, struct regional*
> region,
>         struct module_env* env, struct module_qstate* qstate,
> -       struct iter_env* ie)
> +       struct iter_env* ie, int* msg_lame_empty, int* msg_lame_referral,
> +       int rdset)
>  {
> +       int pkt_before_NS;
>         /* basic sanity checks */
>         log_nametypeclass(VERB_ALGO, "scrub for", zonename,
> LDNS_RR_TYPE_NS,
>                 qinfo->qclass);
> +       *msg_lame_empty = 0;
> +       *msg_lame_referral = 0;
>         if(msg->qdcount > 1)
>                 return 0;
>         if( !(msg->flags&BIT_QR) )
> @@ -1166,11 +1244,21 @@ scrub_message(sldns_buffer* pkt, struct
>                         return 0;
>         }
>
> +       /* If the packet contains type NS in authority before scrub,
> +        * like a self referral. With the answer section empty, it
> +        * was not AA, the query was not sent with RD, with NS in auth,
> +        * and no SOA in auth. For a negative answer, type SOA is present.
> +        * This detects certain lameness if after has removed that. */
> +       pkt_before_NS = msg->an_rrsets == 0 &&
> +               !(msg->flags&BIT_AA) && !rdset &&
> +               pkt_contains_ns(msg) && !soa_in_auth(msg);
> +
>         /* normalize the response, this cleans up the additional.  */
>         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, qstate))
> +       if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate,
> +               pkt_before_NS, msg_lame_empty, msg_lame_referral, rdset))
>                 return 0;
>         return 1;
>  }
> Index: iterator/iter_scrub.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_scrub.h,v
> diff -u -p -r1.1.1.3 iter_scrub.h
> --- iterator/iter_scrub.h       12 Apr 2024 15:44:27 -0000      1.1.1.3
> +++ iterator/iter_scrub.h       20 Sep 2026 09:50:48 -0000
> @@ -62,11 +62,16 @@ struct module_qstate;
>   * @param env: module environment with config settings and cache.
>   * @param qstate: for setting errinf for EDE error messages.
>   * @param ie: iterator module environment data.
> + * @param msg_lame_empty: returned true if the empty packet is lame.
> + * @param msg_lame_referral: returned true if the reply has a referral
> before
> + *     scrub.
> + * @param rdset: if RD bit was sent in query sent by unbound.
>   * @return: false if the message is total waste. true if scrubbed with
> success.
>   */
>  int scrub_message(struct sldns_buffer* pkt, struct msg_parse* msg,
>         struct query_info* qinfo, uint8_t* zonename, struct regional*
> regional,
>         struct module_env* env, struct module_qstate* qstate,
> -       struct iter_env* ie);
> +       struct iter_env* ie, int* msg_lame_empty, int* msg_lame_referral,
> +       int rdset);
>
>  #endif /* ITERATOR_ITER_SCRUB_H */
> Index: iterator/iter_utils.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_utils.c,v
> diff -u -p -r1.26 iter_utils.c
> --- iterator/iter_utils.c       26 May 2026 11:14:11 -0000      1.26
> +++ iterator/iter_utils.c       20 Sep 2026 09:50:48 -0000
> @@ -1313,7 +1313,8 @@ iter_lookup_parent_NS_from_cache(struct
>                 log_rrset_key(VERB_ALGO, "found parent-side NS in cache",
> akey);
>                 dp->has_parent_side_NS = 1;
>                 /* and mark the new names as lame */
> -               if(!delegpt_rrset_add_ns(dp, region, akey, 1)) {
> +               if(!delegpt_rrset_add_ns(dp, region, akey, 1,
> +                       deleg_port_number(env))) {
>                         lock_rw_unlock(&akey->entry.lock);
>                         return 0;
>                 }
> @@ -1702,4 +1703,12 @@ iter_make_minimal(struct reply_info* rep
>         rep->ns_numrrsets = 0;
>         rep->ar_numrrsets = 0;
>         rep->rrset_count -= rem;
> +}
> +
> +int
> +deleg_port_number(struct module_env* env)
> +{
> +       if(env->cfg->ssl_upstream)
> +               return env->cfg->ssl_port;
> +       return -1;
>  }
> Index: iterator/iter_utils.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_utils.h,v
> diff -u -p -r1.19 iter_utils.h
> --- iterator/iter_utils.h       26 May 2026 11:14:11 -0000      1.19
> +++ iterator/iter_utils.h       20 Sep 2026 09:50:48 -0000
> @@ -483,4 +483,7 @@ void limit_nsec_ttl(struct dns_msg* msg)
>   */
>  void iter_make_minimal(struct reply_info* rep);
>
> +/** See if we need a different port number */
> +int deleg_port_number(struct module_env* env);
> +
>  #endif /* ITERATOR_ITER_UTILS_H */
> Index: iterator/iterator.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iterator.c,v
> diff -u -p -r1.43 iterator.c
> --- iterator/iterator.c 27 Jul 2026 14:14:39 -0000      1.43
> +++ iterator/iterator.c 20 Sep 2026 09:50:48 -0000
> @@ -1511,6 +1511,7 @@ processInitRequest(struct module_qstate*
>                 verbose(VERB_ALGO, "no-cache set, going to the network");
>                 qstate->no_cache_lookup = 1;
>                 qstate->no_cache_store = 1;
> +               qstate->fwd_stub_no_cache = 1;
>                 msg = NULL;
>         } else if(qstate->blacklist) {
>                 /* if cache, or anything else, was blacklisted then
> @@ -1530,7 +1531,7 @@ processInitRequest(struct module_qstate*
>                         msg = val_neg_getmsg(qstate->env->neg_cache,
> &iq->qchase,
>                                 qstate->region, qstate->env->rrset_cache,
>                                 qstate->env->scratch_buffer,
> -                               *qstate->env->now, 1/*add SOA*/, NULL,
> +                               *qstate->env->now, 1/*add SOA*/, dpname,
>                                 qstate->env->cfg);
>                 }
>                 /* item taken from cache does not match our query name,
> thus
> @@ -2108,7 +2109,7 @@ query_for_targets(struct module_qstate*
>                                         ns->resolved = 1;
>                                 }
>                                 break;
> -               }
> +                       }
>                 }
>                 /* Send the A request. */
>                 if((ie->supports_ipv4 || ie->nat64.use_nat64) &&
> @@ -2130,7 +2131,7 @@ query_for_targets(struct module_qstate*
>                                  * a missing target. */
>                                 ns->resolved = 1;
>                                 break;
> -               }
> +                       }
>                 }
>
>                 /* mark this target as in progress. */
> @@ -2255,7 +2256,7 @@ processLastResort(struct module_qstate*
>                         errinf(qstate, "could not fetch nameserver");
>                         errinf_dname(qstate, "at zone", iq->dp->name);
>                         if(ret == 1)
> -                       return error_response(qstate, id,
> LDNS_RCODE_SERVFAIL);
> +                               return error_response(qstate, id,
> LDNS_RCODE_SERVFAIL);
>                         return error_response_cache(qstate, id,
> LDNS_RCODE_SERVFAIL);
>                 }
>                 iq->num_target_queries += qs;
> @@ -2391,6 +2392,12 @@ processDSNSFind(struct module_qstate* qs
>
>         /* go up one (more) step, until we hit the dp, if so, end */
>         dname_remove_label(&iq->dsns_point, &iq->dsns_point_len);
> +       if(++iq->dsns_count > MAX_DSNS_FIND_COUNT) {
> +               verbose(VERB_QUERY, "DS NS search exceeded %d labels",
> +                       MAX_DSNS_FIND_COUNT);
> +               errinf(qstate, "DS NS search exceeded label limit");
> +               return error_response_cache(qstate, id,
> LDNS_RCODE_SERVFAIL);
> +       }
>         if(query_dname_compare(iq->dsns_point, iq->dp->name) == 0) {
>                 /* there was no inbetween nameserver, use the old
> delegation
>                  * point again.  And this time, because dsns_point is
> nonNULL
> @@ -2809,7 +2816,7 @@ processQueryTargets(struct module_qstate
>                 if((ret=query_for_targets(qstate, iq, ie, id, -1,
> &extra))!=0) {
>                         errinf(qstate, "could not fetch nameservers for
> 0x20 fallback");
>                         if(ret == 1)
> -                       return error_response(qstate, id,
> LDNS_RCODE_SERVFAIL);
> +                               return error_response(qstate, id,
> LDNS_RCODE_SERVFAIL);
>                         return error_response_cache(qstate, id,
> LDNS_RCODE_SERVFAIL);
>                 }
>                 iq->num_target_queries += extra;
> @@ -2961,8 +2968,8 @@ processQueryTargets(struct module_qstate
>                                         errinf(qstate, "could not fetch
> nameserver");
>                                         errinf_dname(qstate, "at zone",
> iq->dp->name);
>                                         if(ret == 1)
> -                                       return error_response(qstate, id,
> -                                               LDNS_RCODE_SERVFAIL);
> +                                               return
> error_response(qstate, id,
> +
>  LDNS_RCODE_SERVFAIL);
>                                         return
> error_response_cache(qstate, id,
>                                                 LDNS_RCODE_SERVFAIL);
>                                 }
> @@ -3073,7 +3080,9 @@ processQueryTargets(struct module_qstate
>
>         /* Do not check ratelimit for forwarding queries or if we already
> got a
>          * pass. */
> -       sq_check_ratelimit = (!(iq->chase_flags & BIT_RD) &&
> !iq->ratelimit_ok);
> +       sq_check_ratelimit = ((!(iq->chase_flags & BIT_RD) &&
> +               !iq->ratelimit_ok));
> +       iq->ratelimit_incremented = 0;
>         /* We have a valid target. */
>         if(verbosity >= VERB_QUERY) {
>                 log_query_info(VERB_QUERY, "sending query:",
> &iq->qinfo_out);
> @@ -3099,7 +3108,8 @@ processQueryTargets(struct module_qstate
>                 iq->dp->name, iq->dp->namelen,
>                 (iq->dp->tcp_upstream || qstate->env->cfg->tcp_upstream),
>                 (iq->dp->ssl_upstream || qstate->env->cfg->ssl_upstream),
> -               target->tls_auth_name, qstate, &sq_was_ratelimited);
> +               target->tls_auth_name, qstate, &sq_was_ratelimited,
> +               &iq->ratelimit_incremented);
>         if(!outq) {
>                 if(sq_was_ratelimited) {
>                         lock_basic_lock(&ie->queries_ratelimit_lock);
> @@ -3137,7 +3147,6 @@ find_NS(struct reply_info* rep, size_t f
>         return NULL;
>  }
>
> -
>  /**
>   * Process the query response. All queries end up at this state first.
> This
>   * process generally consists of analyzing the response and routing the
> @@ -3179,7 +3188,8 @@ processQueryResponse(struct module_qstat
>         orig_empty_nodata_found = iq->empty_nodata_found;
>         type = response_type_from_server(
>                 (int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
> -               iq->response, &iq->qinfo_out, iq->dp,
> &iq->empty_nodata_found);
> +               iq->response, &iq->qinfo_out, iq->dp,
> &iq->empty_nodata_found,
> +               iq->msg_lame_empty, iq->msg_lame_referral);
>         iq->chase_to_rd = 0;
>         /* remove TC flag, if this is erroneously set by TCP upstream */
>         iq->response->rep->flags &= ~BIT_TC;
> @@ -3457,7 +3467,14 @@ processQueryResponse(struct module_qstat
>                 iq->deleg_msg = iq->response;
>                 /* Keep current delegation point for label comparison */
>                 old_dp = iq->dp;
> -               iq->dp = delegpt_from_message(iq->response,
> qstate->region);
> +               /* A referral reply is "pleasant", refund the
> +                * parent dp's rate charge before descending to the child.
> */
> +               if(iq->ratelimit_incremented)
> +                       infra_ratelimit_dec(qstate->env->infra_cache,
> +                               old_dp->name, old_dp->namelen,
> +                               *qstate->env->now);
> +               iq->dp = delegpt_from_message(iq->response, qstate->region,
> +                       deleg_port_number(qstate->env));
>                 if (qstate->env->cfg->qname_minimisation)
>                         iq->minimisation_state = INIT_MINIMISE_STATE;
>                 if(!iq->dp) {
> @@ -3734,7 +3751,8 @@ prime_supers(struct module_qstate* qstat
>         log_assert(qstate->is_priming || foriq->wait_priming_stub);
>         log_assert(qstate->return_rcode == LDNS_RCODE_NOERROR);
>         /* Convert our response to a delegation point */
> -       dp = delegpt_from_message(qstate->return_msg, forq->region);
> +       dp = delegpt_from_message(qstate->return_msg, forq->region,
> +               deleg_port_number(forq->env));
>         if(!dp) {
>                 /* if there is no convertible delegation point, then
>                  * the ANSWER type was (presumably) a negative answer. */
> @@ -3785,7 +3803,8 @@ processPrimeResponse(struct module_qstat
>         iq->response->rep->flags &= ~(BIT_RD|BIT_RA); /* ignore rec-lame */
>         type = response_type_from_server(
>                 (int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd),
> -               iq->response, &iq->qchase, iq->dp, NULL);
> +               iq->response, &iq->qchase, iq->dp, NULL,
> iq->msg_lame_empty,
> +               iq->msg_lame_referral);
>         if(type == RESPONSE_TYPE_ANSWER) {
>                 qstate->return_rcode = LDNS_RCODE_NOERROR;
>                 qstate->return_msg = iq->response;
> @@ -3949,7 +3968,8 @@ processDSNSResponse(struct module_qstate
>
>         /* else, store as DP and continue at querytargets */
>         foriq->state = QUERYTARGETS_STATE;
> -       foriq->dp = delegpt_from_message(qstate->return_msg, forq->region);
> +       foriq->dp = delegpt_from_message(qstate->return_msg, forq->region,
> +               deleg_port_number(forq->env));
>         if(!foriq->dp) {
>                 log_err("out of memory in dsns dp alloc");
>                 errinf(qstate, "malloc failure, in DS search");
> @@ -3998,7 +4018,7 @@ processClassResponse(struct module_qstat
>                 /* if there are records, copy RCODE */
>                 /* lower sec_state if this message is lower */
>                 if(from->rep->rrset_count != 0) {
> -                       size_t n =
> from->rep->rrset_count+to->rep->rrset_count;
> +                       size_t i, n =
> from->rep->rrset_count+to->rep->rrset_count;
>                         struct ub_packed_rrset_key** dest, **d;
>                         /* copy appropriate rcode */
>                         to->rep->flags = from->rep->flags;
> @@ -4020,24 +4040,49 @@ processClassResponse(struct module_qstat
>                         memcpy(dest, to->rep->rrsets, to->rep->an_numrrsets
>                                 * sizeof(dest[0]));
>                         dest += to->rep->an_numrrsets;
> -                       memcpy(dest, from->rep->rrsets,
> from->rep->an_numrrsets
> -                               * sizeof(dest[0]));
> +                       for(i=0; i<from->rep->an_numrrsets; i++) {
> +                               dest[i] = packed_rrset_copy_region(
> +                                       from->rep->rrsets[i],
> forq->region, 0);
> +                               if(!dest[i]) {
> +                                       log_err("malloc failed in collect
> ANY");
> +                                       foriq->state = FINISHED_STATE;
> +                                       return;
> +                               }
> +                       }
>                         dest += from->rep->an_numrrsets;
>                         /* copy NS */
>                         memcpy(dest, to->rep->rrsets+to->rep->an_numrrsets,
>                                 to->rep->ns_numrrsets * sizeof(dest[0]));
>                         dest += to->rep->ns_numrrsets;
> -                       memcpy(dest,
> from->rep->rrsets+from->rep->an_numrrsets,
> -                               from->rep->ns_numrrsets * sizeof(dest[0]));
> +                       for(i=0; i<from->rep->ns_numrrsets; i++) {
> +                               dest[i] = packed_rrset_copy_region(
> +                                       from->rep->rrsets[
> +                                       from->rep->an_numrrsets+i],
> +                                       forq->region, 0);
> +                               if(!dest[i]) {
> +                                       log_err("malloc failed in collect
> ANY");
> +                                       foriq->state = FINISHED_STATE;
> +                                       return;
> +                               }
> +                       }
>                         dest += from->rep->ns_numrrsets;
>                         /* copy AR */
>                         memcpy(dest, to->rep->rrsets+to->rep->an_numrrsets+
>                                 to->rep->ns_numrrsets,
>                                 to->rep->ar_numrrsets * sizeof(dest[0]));
>                         dest += to->rep->ar_numrrsets;
> -                       memcpy(dest,
> from->rep->rrsets+from->rep->an_numrrsets+
> -                               from->rep->ns_numrrsets,
> -                               from->rep->ar_numrrsets * sizeof(dest[0]));
> +                       for(i=0; i<from->rep->ar_numrrsets; i++) {
> +                               dest[i] = packed_rrset_copy_region(
> +                                       from->rep->rrsets[
> +                                       from->rep->an_numrrsets+
> +                                       from->rep->ns_numrrsets+i],
> +                                       forq->region, 0);
> +                               if(!dest[i]) {
> +                                       log_err("malloc failed in collect
> ANY");
> +                                       foriq->state = FINISHED_STATE;
> +                                       return;
> +                               }
> +                       }
>                         /* update counts */
>                         to->rep->rrsets = d;
>                         to->rep->an_numrrsets += from->rep->an_numrrsets;
> @@ -4395,7 +4440,10 @@ process_response(struct module_qstate* q
>
>         /* normalize and sanitize: easy to delete items from linked lists
> */
>         if(!scrub_message(pkt, prs, &iq->qinfo_out, iq->dp->name,
> -               qstate->env->scratch, qstate->env, qstate, ie)) {
> +               qstate->env->scratch, qstate->env, qstate, ie,
> +               &iq->msg_lame_empty, &iq->msg_lame_referral,
> +               (int)((iq->chase_flags&BIT_RD) || iq->chase_to_rd)
> +               )) {
>                 /* if 0x20 enabled, start fallback, but we have no message
> */
>                 if(event == module_event_capsfail && !iq->caps_fallback) {
>                         iq->caps_fallback = 1;
> Index: iterator/iterator.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/iterator/iterator.h,v
> diff -u -p -r1.24 iterator.h
> --- iterator/iterator.h 31 Aug 2025 21:41:09 -0000      1.24
> +++ iterator/iterator.h 20 Sep 2026 09:50:48 -0000
> @@ -104,6 +104,11 @@ extern int BLACKLIST_PENALTY;
>  #define RTT_BAND 400
>  /** Number of retries for empty nodata packets before it is accepted. */
>  #define EMPTY_NODATA_RETRY_COUNT 2
> +/** max label-strip iterations in DSNS_FIND_STATE (RFC 4035 4.2 parent-NS
> + * search) before giving up; bounds upstream NS sends per client DS.
> + * Means the max number of labels in grandchild to the grandparent zone
> that
> + * are co-hosted. */
> +#define MAX_DSNS_FIND_COUNT    20
>
>  /**
>   * Iterator global state for nat64.
> @@ -375,6 +380,10 @@ struct iter_qstate {
>         /** if true, already tested for ratelimiting and passed the test */
>         int ratelimit_ok;
>
> +       /** If the last query, that may be a referral, incremented the
> +        * ratelimit counter. */
> +       int ratelimit_incremented;
> +
>         /**
>          * The query must store NS records from referrals as parentside RRs
>          * Enabled once it hits resolution problems, to throttle retries.
> @@ -399,6 +408,8 @@ struct iter_qstate {
>         uint8_t* dsns_point;
>         /** length of the dname in dsns_point */
>         size_t dsns_point_len;
> +       /** number of label-strip iterations performed in DSNS_FIND_STATE
> */
> +       int dsns_count;
>
>         /**
>          * expected dnssec information for this iteration step.
> @@ -433,6 +444,13 @@ struct iter_qstate {
>          * This flag detects that a completely empty nodata was received,
>          * already so that it is accepted later. */
>         int empty_nodata_found;
> +
> +       /** Store if the answer was empty, but lame, before it became
> empty.*/
> +       int msg_lame_empty;
> +
> +       /** Store if the answer was a referral, to self, before scrub. So
> the
> +        * it is not some sort of answer. */
> +       int msg_lame_referral;
>
>         /** list of pending queries to authoritative servers. */
>         struct outbound_list outlist;
> Index: libunbound/context.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/libunbound/context.h,v
> diff -u -p -r1.9 context.h
> --- libunbound/context.h        5 Sep 2023 11:12:10 -0000       1.9
> +++ libunbound/context.h        20 Sep 2026 09:50:48 -0000
> @@ -167,6 +167,8 @@ struct ctx_query {
>          ub_event_callback_type cb_event;
>         /** for async query, the callback user arg */
>         void* cb_arg;
> +       /** for async query the unique info */
> +       void* unique_info;
>
>         /** answer message, result from resolver lookup. */
>         uint8_t* msg;
> Index: libunbound/libunbound.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/libunbound/libunbound.c,v
> diff -u -p -r1.22 libunbound.c
> --- libunbound/libunbound.c     4 Sep 2024 09:36:40 -0000       1.22
> +++ libunbound/libunbound.c     20 Sep 2026 09:50:48 -0000
> @@ -571,6 +571,8 @@ ub_ctx_async(struct ub_ctx* ctx, int dot
>  int
>  ub_poll(struct ub_ctx* ctx)
>  {
> +       if(!ctx || ctx->event_base)
> +               return UB_INITFAIL;
>         /* no need to hold lock while testing for readability. */
>         return tube_poll(ctx->rr_pipe);
>  }
> @@ -578,6 +580,8 @@ ub_poll(struct ub_ctx* ctx)
>  int
>  ub_fd(struct ub_ctx* ctx)
>  {
> +       if(!ctx || ctx->event_base)
> +               return -1;
>         return tube_read_fd(ctx->rr_pipe);
>  }
>
> @@ -672,6 +676,8 @@ ub_process(struct ub_ctx* ctx)
>         int r;
>         uint8_t* msg;
>         uint32_t len;
> +       if(!ctx || ctx->event_base)
> +               return UB_INITFAIL;
>         while(1) {
>                 msg = NULL;
>                 lock_basic_lock(&ctx->rrpipe_lock);
> @@ -700,6 +706,8 @@ ub_wait(struct ub_ctx* ctx)
>         int r;
>         uint8_t* msg;
>         uint32_t len;
> +       if(!ctx || ctx->event_base)
> +               return UB_INITFAIL;
>         /* this is basically the same loop as _process(), but with changes.
>          * holds the rrpipe lock and waits with tube_wait */
>         while(1) {
> @@ -837,6 +845,8 @@ ub_resolve_async(struct ub_ctx* ctx, con
>         struct ctx_query* q;
>         uint8_t* msg = NULL;
>         uint32_t len = 0;
> +       if(!ctx || ctx->event_base)
> +               return UB_INITFAIL;
>
>         if(async_id)
>                 *async_id = 0;
> @@ -1467,8 +1477,15 @@ ub_ctx_set_event(struct ub_ctx* ctx, str
>
>         lock_basic_lock(&ctx->cfglock);
>         /* destroy the current worker - safe to pass in NULL */
> +
> +       /* Unlock the cfglock during libworker_delete_event, since it
> +        * calls context_release_alloc, that wants to lock cfglock again.
> +        * Since the event base is used from one thread, the one that
> +        * called this function, it is safe to do so. */
> +       lock_basic_unlock(&ctx->cfglock);
>         libworker_delete_event(ctx->event_worker);
>         ctx->event_worker = NULL;
> +       lock_basic_lock(&ctx->cfglock);
>         new_base = ub_libevent_event_base(base);
>         if (new_base)
>                 ctx->event_base = new_base;
> Index: libunbound/libworker.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/libunbound/libworker.c,v
> diff -u -p -r1.37 libworker.c
> --- libunbound/libworker.c      27 Jul 2026 14:14:39 -0000      1.37
> +++ libunbound/libworker.c      20 Sep 2026 09:50:48 -0000
> @@ -651,7 +651,8 @@ int libworker_fg(struct ub_ctx* ctx, str
>         }
>         /* process new query */
>         if(!mesh_new_callback(w->env->mesh, &qinfo, qflags, &edns,
> -               w->back->udp_buff, qid, libworker_fg_done_cb, q, 0)) {
> +               w->back->udp_buff, qid, libworker_fg_done_cb, q, 0,
> +               &q->unique_info)) {
>                 free(qinfo.qname);
>                 return UB_NOMEM;
>         }
> @@ -732,7 +733,8 @@ int libworker_attach_mesh(struct ub_ctx*
>         if(async_id)
>                 *async_id = q->querynum;
>         if(!mesh_new_callback(w->env->mesh, &qinfo, qflags, &edns,
> -               w->back->udp_buff, qid, libworker_event_done_cb, q, 0)) {
> +               w->back->udp_buff, qid, libworker_event_done_cb, q, 0,
> +               &q->unique_info)) {
>                 free(qinfo.qname);
>                 return UB_NOMEM;
>         }
> @@ -870,7 +872,8 @@ handle_newq(struct libworker* w, uint8_t
>         q->w = w;
>         /* process new query */
>         if(!mesh_new_callback(w->env->mesh, &qinfo, qflags, &edns,
> -               w->back->udp_buff, qid, libworker_bg_done_cb, q, 0)) {
> +               w->back->udp_buff, qid, libworker_bg_done_cb, q, 0,
> +               &q->unique_info)) {
>                 add_bg_result(w, q, NULL, UB_NOMEM, NULL, 0);
>         }
>         free(qinfo.qname);
> @@ -888,7 +891,8 @@ struct outbound_entry* libworker_send_qu
>         int check_ratelimit,
>         struct sockaddr_storage* addr, socklen_t addrlen, uint8_t* zone,
>         size_t zonelen, int tcp_upstream, int ssl_upstream, char*
> tls_auth_name,
> -       struct module_qstate* q, int* was_ratelimited)
> +       struct module_qstate* q, int* was_ratelimited,
> +       int* ratelimit_incremented)
>  {
>         struct libworker* w = (struct libworker*)q->env->worker;
>         struct outbound_entry* e = (struct outbound_entry*)regional_alloc(
> @@ -900,7 +904,7 @@ struct outbound_entry* libworker_send_qu
>                 want_dnssec, nocaps, check_ratelimit, tcp_upstream,
> ssl_upstream,
>                 tls_auth_name, addr, addrlen, zone, zonelen, q,
>                 libworker_handle_service_reply, e, w->back->udp_buff,
> q->env,
> -               was_ratelimited);
> +               was_ratelimited, ratelimit_incremented);
>         if(!e->qsent) {
>                 return NULL;
>         }
> @@ -985,7 +989,8 @@ struct outbound_entry* worker_send_query
>         struct sockaddr_storage* ATTR_UNUSED(addr), socklen_t
> ATTR_UNUSED(addrlen),
>         uint8_t* ATTR_UNUSED(zone), size_t ATTR_UNUSED(zonelen), int
> ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> Index: libunbound/remote.h
> ===================================================================
> RCS file: libunbound/remote.h
> diff -N libunbound/remote.h
> --- /dev/null   1 Jan 1970 00:00:00 -0000
> +++ libunbound/remote.h 20 Sep 2026 09:50:48 -0000
> @@ -0,0 +1,65 @@
> +/*
> + * libunbound/remote.h - prototypes for remote control methods.
> + *
> + * Copyright (c) 2026, NLnet Labs. All rights reserved.
> + *
> + * This software is open source.
> + *
> + * Redistribution and use in source and binary forms, with or without
> + * modification, are permitted provided that the following conditions
> + * are met:
> + *
> + * Redistributions of source code must retain the above copyright notice,
> + * this list of conditions and the following disclaimer.
> + *
> + * Redistributions in binary form must reproduce the above copyright
> notice,
> + * this list of conditions and the following disclaimer in the
> documentation
> + * and/or other materials provided with the distribution.
> + *
> + * Neither the name of the NLNET LABS nor the names of its contributors
> may
> + * be used to endorse or promote products derived from this software
> without
> + * specific prior written permission.
> + *
> + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
> + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
> + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
> + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
> + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
> + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
> LIMITED
> + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
> + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
> + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
> + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
> + */
> +
> +/**
> + * \file
> + *
> + * This file declares the methods that must be implemented to use the
> + * remote control service.
> + */
> +
> +#ifndef LIBUNBOUND_REMOTE_H
> +#define LIBUNBOUND_REMOTE_H
> +
> +struct comm_reply;
> +struct comm_point;
> +
> +/** fast reload thread commands to remote service thread event callback */
> +void fast_reload_service_cb(int fd, short bits, void* arg);
> +
> +/** fast reload callback for the remote control client connection */
> +int fast_reload_client_callback(struct comm_point* c, void* arg, int err,
> +       struct comm_reply* rep);
> +
> +/** handle remote control accept callbacks */
> +int remote_accept_callback(struct comm_point*, void*, int, struct
> comm_reply*);
> +
> +/** handle remote control data callbacks */
> +int remote_control_callback(struct comm_point*, void*, int, struct
> comm_reply*);
> +
> +/** routine to printout option values over SSL */
> +void  remote_get_opt_ssl(char* line, void* arg);
> +
> +#endif /* LIBUNBOUND_REMOTE_H */
> Index: libunbound/worker.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/libunbound/worker.h,v
> diff -u -p -r1.7 worker.h
> --- libunbound/worker.h 23 Feb 2022 12:04:05 -0000      1.7
> +++ libunbound/worker.h 20 Sep 2026 09:50:48 -0000
> @@ -70,6 +70,8 @@ struct query_info;
>   * @param q: which query state to reactivate upon return.
>   * @param was_ratelimited: it will signal back if the query failed to
> pass the
>   *     ratelimit check.
> + * @param ratelimit_incremented: set to true if the ratelimit counter
> + *     was increased.
>   * @return: false on failure (memory or socket related). no query was
>   *      sent.
>   */
> @@ -78,7 +80,8 @@ struct outbound_entry* libworker_send_qu
>         int check_ratelimit,
>         struct sockaddr_storage* addr, socklen_t addrlen, uint8_t* zone,
>         size_t zonelen, int tcp_upstream, int ssl_upstream, char*
> tls_auth_name,
> -       struct module_qstate* q, int* was_ratelimited);
> +       struct module_qstate* q, int* was_ratelimited,
> +       int* ratelimit_incremented);
>
>  /** process incoming serviced query replies from the network */
>  int libworker_handle_service_reply(struct comm_point* c, void* arg, int
> error,
> @@ -126,6 +129,8 @@ void worker_sighandler(int sig, void* ar
>   * @param q: which query state to reactivate upon return.
>   * @param was_ratelimited: it will signal back if the query failed to
> pass the
>   *     ratelimit check.
> + * @param ratelimit_incremented: set to true if the ratelimit counter
> + *     was increased.
>   * @return: false on failure (memory or socket related). no query was
>   *      sent.
>   */
> @@ -134,7 +139,8 @@ struct outbound_entry* worker_send_query
>         int check_ratelimit,
>         struct sockaddr_storage* addr, socklen_t addrlen, uint8_t* zone,
>         size_t zonelen, int tcp_upstream, int ssl_upstream, char*
> tls_auth_name,
> -       struct module_qstate* q, int* was_ratelimited);
> +       struct module_qstate* q, int* was_ratelimited,
> +       int* ratelimit_incremented);
>
>  /**
>   * process control messages from the main thread. Frees the control
> @@ -170,14 +176,5 @@ void worker_start_accept(void* arg);
>
>  /** stop accept callback handler */
>  void worker_stop_accept(void* arg);
> -
> -/** handle remote control accept callbacks */
> -int remote_accept_callback(struct comm_point*, void*, int, struct
> comm_reply*);
> -
> -/** handle remote control data callbacks */
> -int remote_control_callback(struct comm_point*, void*, int, struct
> comm_reply*);
> -
> -/** routine to printout option values over SSL */
> -void  remote_get_opt_ssl(char* line, void* arg);
>
>  #endif /* LIBUNBOUND_WORKER_H */
> Index: respip/respip.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/respip/respip.c,v
> diff -u -p -r1.19 respip.c
> --- respip/respip.c     27 Jul 2026 14:14:39 -0000      1.19
> +++ respip/respip.c     20 Sep 2026 09:50:48 -0000
> @@ -899,27 +899,34 @@ respip_rewrite_reply(const struct query_
>         int rpz_cname_override = 0;
>         char* log_name = NULL;
>
> -       if(!cinfo)
> -               goto done;
> -       ctaglist = cinfo->taglist;
> -       ctaglen = cinfo->taglen;
> -       tag_actions = cinfo->tag_actions;
> -       tag_actions_size = cinfo->tag_actions_size;
> -       tag_datas = cinfo->tag_datas;
> -       tag_datas_size = cinfo->tag_datas_size;
> -       if(cinfo->view) {
> -               view = cinfo->view;
> -               lock_rw_rdlock(&view->lock);
> -       } else if(cinfo->view_name) {
> -               view = views_find_view(views, cinfo->view_name, 0);
> -               if(!view) {
> -                       /* If the view no longer exists, the rewrite can
> not
> -                        * be processed further. */
> -                       verbose(VERB_ALGO, "respip: failed because view %s
> no "
> -                               "longer exists", cinfo->view_name);
> -                       return 0;
> +       if(!cinfo) {
> +               /* Internal mesh sub-query (e.g. dns64 A lookup): no
> +                * per-client view/tags, but global response-ip and RPZ
> +                * rpz-ip must still apply. */
> +               ctaglist = NULL; ctaglen = 0;
> +               tag_actions = NULL; tag_actions_size = 0;
> +               tag_datas = NULL; tag_datas_size = 0;
> +       } else {
> +               ctaglist = cinfo->taglist;
> +               ctaglen = cinfo->taglen;
> +               tag_actions = cinfo->tag_actions;
> +               tag_actions_size = cinfo->tag_actions_size;
> +               tag_datas = cinfo->tag_datas;
> +               tag_datas_size = cinfo->tag_datas_size;
> +               if(cinfo->view) {
> +                       view = cinfo->view;
> +                       lock_rw_rdlock(&view->lock);
> +               } else if(cinfo->view_name) {
> +                       view = views_find_view(views, cinfo->view_name, 0);
> +                       if(!view) {
> +                               /* If the view no longer exists, the
> rewrite can not
> +                                * be processed further. */
> +                               verbose(VERB_ALGO, "respip: failed because
> view %s no "
> +                                       "longer exists", cinfo->view_name);
> +                               return 0;
> +                       }
> +                       /* The view is rdlocked by views_find_view. */
>                 }
> -               /* The view is rdlocked by views_find_view. */
>         }
>
>         log_assert(ipset);
> @@ -1157,8 +1164,10 @@ respip_operate(struct module_qstate* qst
>                                  * clients. */
>                                 qstate->is_drop = 1;
>                         } else if(alias_rrset) {
> -                               if(!generate_cname_request(qstate,
> alias_rrset))
> +                               if(!generate_cname_request(qstate,
> alias_rrset)) {
> +                                       errinf(qstate, "Could not generate
> CNAME request");
>                                         goto servfail;
> +                               }
>                                 next_state = module_wait_subquery;
>                         }
>                         qstate->return_msg->rep = new_rep;
> @@ -1172,6 +1181,7 @@ respip_operate(struct module_qstate* qst
>    servfail:
>         qstate->return_rcode = LDNS_RCODE_SERVFAIL;
>         qstate->return_msg = NULL;
> +       qstate->ext_state[id] = module_finished;
>  }
>
>  int
> @@ -1268,6 +1278,7 @@ respip_inform_super(struct module_qstate
>         return;
>
>    fail:
> +       errinf(super, "CNAME lookup failed");
>         super->return_rcode = LDNS_RCODE_SERVFAIL;
>         super->return_msg = NULL;
>         return;
> Index: services/authzone.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/authzone.c,v
> diff -u -p -r1.33 authzone.c
> --- services/authzone.c 27 Jul 2026 14:14:39 -0000      1.33
> +++ services/authzone.c 20 Sep 2026 09:50:48 -0000
> @@ -55,6 +55,7 @@
>  #include "util/log.h"
>  #include "util/module.h"
>  #include "util/random.h"
> +#include "util/timeval_func.h"
>  #include "services/cache/dns.h"
>  #include "services/outside_network.h"
>  #include "services/listen_dnsport.h"
> @@ -95,6 +96,8 @@
>  /** number of timeouts before we fallback from IXFR to AXFR,
>   * because some versions of servers (eg. dnsmasq) drop IXFR packets. */
>  #define NUM_TIMEOUTS_FALLBACK_IXFR 3
> +/** number of IXFRs before an AXFR is performed, to consolidate RPZ
> memory. */
> +#define NUM_IXFR_BEFORE_AXFR 5
>
>  /** pick up nextprobe task to start waiting to perform transfer actions */
>  static void xfr_set_timeout(struct auth_xfer* xfr, struct module_env* env,
> @@ -106,6 +109,9 @@ static void xfr_probe_send_or_end(struct
>   * or transfer task if nothing to probe, or false if already in progress
> */
>  static int xfr_start_probe(struct auth_xfer* xfr, struct module_env* env,
>         struct auth_master* spec);
> +/** copy the master addresses from the task_probe lookups to the
> allow_notify
> + * list of masters */
> +static void probe_copy_masters_for_allow_notify(struct auth_xfer* xfr);
>  /** delete xfer structure (not its tree entry) */
>  void auth_xfer_delete(struct auth_xfer* xfr);
>
> @@ -386,6 +392,20 @@ auth_data_del(rbnode_type* n, void* ATTR
>         auth_data_delete(z);
>  }
>
> +/** delete chunklist */
> +static void
> +auth_chunk_list_delete(struct auth_chunk* first)
> +{
> +       struct auth_chunk* c, *cn;
> +       c = first;
> +       while(c) {
> +               cn = c->next;
> +               free(c->data);
> +               free(c);
> +               c = cn;
> +       }
> +}
> +
>  /** delete an auth zone structure (tree remove must be done elsewhere) */
>  static void
>  auth_zone_delete(struct auth_zone* z, struct auth_zones* az)
> @@ -407,6 +427,7 @@ auth_zone_delete(struct auth_zone* z, st
>         }
>         if(z->rpz)
>                 rpz_delete(z->rpz);
> +       auth_chunk_list_delete(z->perform_write_chunk_list);
>         free(z->name);
>         free(z->zonefile);
>         free(z);
> @@ -432,7 +453,12 @@ auth_zone_create(struct auth_zones* az,
>         rbtree_init(&z->data, &auth_data_cmp);
>         lock_rw_init(&z->lock);
>         lock_protect(&z->lock, &z->name, sizeof(*z)-sizeof(rbnode_type)-
> -                       sizeof(&z->rpz_az_next)-sizeof(&z->rpz_az_prev));
> +                       sizeof(z->rpz_az_next)-sizeof(z->rpz_az_prev)-
> +
>  sizeof(z->max_transfer_size)-sizeof(z->max_transfer_size));
> +       lock_protect(&z->lock, &z->max_transfer_size,
> +               sizeof(z->max_transfer_size));
> +       lock_protect(&z->lock, &z->max_transfer_time,
> +               sizeof(z->max_transfer_time));
>         lock_rw_wrlock(&z->lock);
>         /* z lock protects all, except rbtree itself and the rpz linked
> list
>          * pointers, which are protected using az->lock */
> @@ -1175,6 +1201,22 @@ az_insert_rr(struct auth_zone* z, uint8_
>                 log_err("wrong class for RR");
>                 return 0;
>         }
> +       if(rr_type == LDNS_RR_TYPE_A && rdatalen != 6 /* 2 + 4 */) {
> +               log_err("malformed A record");
> +               return 0;
> +       } else if(rr_type == LDNS_RR_TYPE_AAAA && rdatalen != 18 /* 2 + 16
> */) {
> +               log_err("malformed AAAA record");
> +               return 0;
> +       }
> +       if(!dname_subdomain_c(dname, z->name)) {
> +               char nm[LDNS_MAX_DOMAINLEN], zn[LDNS_MAX_DOMAINLEN];
> +               dname_str(dname, nm);
> +               dname_str(z->name, zn);
> +               verbose(VERB_ALGO, "auth-zone %s: dropping out-of-zone RR "
> +                       "%s", zn, nm);
> +               if(duplicate) *duplicate=1; /* treat as bad insert */
> +               return 1;
> +       }
>         if(!(node=az_domain_find_or_create(z, dname, dname_len))) {
>                 log_err("cannot create domain");
>                 return 0;
> @@ -1182,6 +1224,10 @@ az_insert_rr(struct auth_zone* z, uint8_
>         if(!az_domain_add_rr(node, rr_type, rr_ttl, rdata, rdatalen,
>                 duplicate)) {
>                 log_err("cannot add RR to domain");
> +               if(node->rrsets == NULL) {
> +                       (void)rbtree_delete(&z->data, node);
> +                       auth_data_delete(node);
> +               }
>                 return 0;
>         }
>         if(z->rpz) {
> @@ -1505,6 +1551,11 @@ az_parse_file(struct auth_zone* z, FILE*
>                                           "exceeded", fname,
> state->lineno);
>                                         return 0;
>                                 }
> +                               /* A $INCLUDE is not expected for a
> secondary zone. */
> +                               if(z->zone_is_slave) {
> +                                       log_err("%s:%d $INCLUDE not
> allowed for secondary zone", fname, state->lineno);
> +                                       return 0;
> +                               }
>                                 /* skip spaces */
>                                 while(*incfile == ' ' || *incfile == '\t')
>                                         incfile++;
> @@ -1570,6 +1621,16 @@ az_parse_file(struct auth_zone* z, FILE*
>         return 1;
>  }
>
> +void auth_zone_clear_data(struct auth_zone* z)
> +{
> +       /* clear the data tree */
> +       traverse_postorder(&z->data, auth_data_del, NULL);
> +       rbtree_init(&z->data, &auth_data_cmp);
> +       /* clear the RPZ policies */
> +       if(z->rpz)
> +               rpz_clear(z->rpz);
> +}
> +
>  int
>  auth_zone_read_zonefile(struct auth_zone* z, struct config_file* cfg)
>  {
> @@ -1592,10 +1653,16 @@ auth_zone_read_zonefile(struct auth_zone
>         in = fopen(zfilename, "r");
>         if(!in) {
>                 char* n = sldns_wire2str_dname(z->name, z->namelen);
> -               if(z->zone_is_slave && errno == ENOENT) {
> -                       /* we fetch the zone contents later, no file yet */
> -                       verbose(VERB_ALGO, "no zonefile %s for %s",
> -                               zfilename, n?n:"error");
> +               if(errno == ENOENT) {
> +                       /* For a secondary, fetch the zone contents later,
> no
> +                        * file yet. For a primary, no way to fetch the
> zone,
> +                        * so warn. */
> +                       if(z->zone_is_slave)
> +                               verbose(VERB_ALGO, "no zonefile %s for %s",
> +                                       zfilename, n?n:"error");
> +                       else
> +                               log_warn("no zonefile %s for %s",
> +                                       zfilename, n?n:"error");
>                         free(n);
>                         return 1;
>                 }
> @@ -1798,9 +1865,11 @@ auth_zones_read_zones(struct auth_zones*
>         RBTREE_FOR(z, struct auth_zone*, &az->ztree) {
>                 lock_rw_wrlock(&z->lock);
>                 if(!auth_zone_read_zonefile(z, cfg)) {
> +                       /* For both secondary and primary zones, not fatal.
> +                        * This keeps the server up. */
> +                       auth_zone_clear_data(z);
>                         lock_rw_unlock(&z->lock);
> -                       lock_rw_unlock(&az->lock);
> -                       return 0;
> +                       continue;
>                 }
>                 if(z->zonefile && z->zonefile[0]!=0 && env)
>                         zonemd_offline_verify(z, env, mods);
> @@ -2076,6 +2145,7 @@ auth_xfer_setup(struct auth_zone* z, str
>         if(!xfr_find_soa(z, x)) {
>                 return 1;
>         }
> +       x->is_rpz = (z->rpz!=NULL);
>         /* nothing for probe, nextprobe and transfer tasks */
>         return 1;
>  }
> @@ -2135,6 +2205,9 @@ auth_zones_cfg(struct auth_zones* az, st
>                 }
>                 return 0;
>         }
> +       /* Populate the xfer related options early since we may create one
> now */
> +       z->max_transfer_size = c->max_transfer_size;
> +       z->max_transfer_time = c->max_transfer_time;
>         if(c->masters || c->urls) {
>                 if(!(x=auth_zones_find_or_add_xfer(az, z))) {
>                         lock_rw_unlock(&az->lock);
> @@ -2168,7 +2241,12 @@ auth_zones_cfg(struct auth_zones* az, st
>         z->zonemd_reject_absence = c->zonemd_reject_absence;
>         if(c->isrpz && !z->rpz){
>                 if(!(z->rpz = rpz_create(c))){
> -                       fatal_exit("Could not setup RPZ zones");
> +                       log_err("Could not setup RPZ zones");
> +                       if(x) {
> +                               lock_basic_unlock(&x->lock);
> +                       }
> +                       lock_rw_unlock(&z->lock);
> +                       lock_rw_unlock(&az->rpz_lock);
>                         return 0;
>                 }
>                 lock_protect(&z->lock, &z->rpz->local_zones,
> sizeof(*z->rpz));
> @@ -2206,6 +2284,10 @@ auth_zones_cfg(struct auth_zones* az, st
>                         lock_rw_unlock(&z->lock);
>                         return 0;
>                 }
> +               /* Pick up allow notify entries, early. This works for
> +                * addresses and netblocks. */
> +               if(!x->allow_notify_list)
> +                       probe_copy_masters_for_allow_notify(x);
>                 lock_basic_unlock(&x->lock);
>         }
>
> @@ -2302,17 +2384,11 @@ static void
>  auth_chunks_delete(struct auth_transfer* at)
>  {
>         if(at->chunks_first) {
> -               struct auth_chunk* c, *cn;
> -               c = at->chunks_first;
> -               while(c) {
> -                       cn = c->next;
> -                       free(c->data);
> -                       free(c);
> -                       c = cn;
> -               }
> +               auth_chunk_list_delete(at->chunks_first);
>         }
>         at->chunks_first = NULL;
>         at->chunks_last = NULL;
> +       at->chunks_total = 0;
>  }
>
>  /** free master addr list */
> @@ -2644,7 +2720,7 @@ az_empty_nonterminal(struct auth_zone* z
>         while(next && (rbnode_type*)next != RBTREE_NULL && next->rrsets ==
> NULL) {
>                 /* the next name has empty rrsets, is an empty nonterminal
>                  * itself, see if there exists something below it */
> -               next = (struct auth_data*)rbtree_next(&node->node);
> +               next = (struct auth_data*)rbtree_next(&next->node);
>         }
>         if((rbnode_type*)next == RBTREE_NULL || !next) {
>                 /* there is no next node, so something below it cannot
> @@ -3525,7 +3601,13 @@ int auth_zones_lookup(struct auth_zones*
>                 *fallback = 1;
>                 return 0;
>         }
> -       if(z->zone_expired) {
> +       if(z->zone_expired || (z->zonemd_check && z->zonemd_callback_env))
> {
> +               /* Do not serve from a zonemd-check zone while its ZONEMD
> +                * verification is still pending: the content is not yet
> known
> +                * to pass the configured check. The pending marker
> +                * (zonemd_callback_env) is set under z->lock when the
> async
> +                * lookup is spawned and cleared by the callback under
> z->lock,
> +                * so this test is race-free. */
>                 *fallback = z->fallback_enabled;
>                 lock_rw_unlock(&z->lock);
>                 return 0;
> @@ -3627,7 +3709,10 @@ int auth_zones_downstream_answer(struct
>                 lock_rw_unlock(&z->lock);
>                 return 0;
>         }
> -       if(z->zone_expired) {
> +       if(z->zone_expired || (z->zonemd_check && z->zonemd_callback_env))
> {
> +               /* see auth_zones_lookup: a pending ZONEMD verification is
> +                * treated like expiry - the zone content is not yet known
> +                * to pass the configured check. */
>                 if(z->fallback_enabled) {
>                         lock_rw_unlock(&z->lock);
>                         return 0;
> @@ -4298,7 +4383,7 @@ xfr_create_ixfr_packet(struct auth_xfer*
>  {
>         struct query_info qinfo;
>         uint32_t serial;
> -       int have_zone;
> +       int have_zone, get_full = 0;
>         have_zone = xfr->have_zone;
>         serial = xfr->serial;
>
> @@ -4311,7 +4396,18 @@ xfr_create_ixfr_packet(struct auth_xfer*
>         xfr->task_transfer->on_ixfr_is_axfr = 0;
>         xfr->task_transfer->on_ixfr = 1;
>         qinfo.qtype = LDNS_RR_TYPE_IXFR;
> -       if(!have_zone || xfr->task_transfer->ixfr_fail || !master->ixfr) {
> +       if(xfr->num_ixfrs >= NUM_IXFR_BEFORE_AXFR && xfr->is_rpz) {
> +               /* For the RPZ, an IXFR is going to grow regions, and a
> +                * full transfer, zonefile read, AXFR and HTTP clear the
> +                * region, but IXFR does not. That memory keeps growing,
> +                * and getting a full transfer with AXFR here resets that.
> +                * The rpz->client_set->region, rpz->ns_set->region and
> +                * rpz->respip_set->region need to be reset, they are for
> +                * rpz-client-ip, rpz-nsip and rpz-ip. */
> +               get_full = 1;
> +       }
> +       if(!have_zone || xfr->task_transfer->ixfr_fail || !master->ixfr
> +               || get_full) {
>                 qinfo.qtype = LDNS_RR_TYPE_AXFR;
>                 xfr->task_transfer->ixfr_fail = 0;
>                 xfr->task_transfer->on_ixfr = 0;
> @@ -4462,29 +4558,31 @@ chunkline_get_line(struct auth_chunk** c
>  }
>
>  /** count number of open and closed parenthesis in a chunkline */
> -static int
> +int
>  chunkline_count_parens(sldns_buffer* buf, size_t start)
>  {
>         size_t end = sldns_buffer_position(buf);
>         size_t i;
>         int count = 0;
> -       int squote = 0, dquote = 0;
> +       int dquote = 0;
> +       char prev_c = 0;
>         for(i=start; i<end; i++) {
>                 char c = (char)sldns_buffer_read_u8_at(buf, i);
> -               if(squote && c != '\'') continue;
> -               if(dquote && c != '"') continue;
> -               if(c == '"')
> +               if(dquote && !(c == '"' && prev_c != '\\')) {
> +                       prev_c = (prev_c == '\\' && c == '\\') ? 0 : c;
> +                       continue;
> +               }
> +               if(c == '"' && prev_c != '\\')
>                         dquote = !dquote; /* skip quoted part */
> -               else if(c == '\'')
> -                       squote = !squote; /* skip quoted part */
> -               else if(c == '(')
> +               else if(c == '(' && prev_c != '\\')
>                         count ++;
> -               else if(c == ')')
> +               else if(c == ')' && prev_c != '\\')
>                         count --;
> -               else if(c == ';') {
> +               else if(c == ';' && prev_c != '\\') {
>                         /* rest is a comment */
>                         return count;
>                 }
> +               prev_c = (prev_c == '\\' && c == '\\') ? 0 : c;
>         }
>         return count;
>  }
> @@ -4495,20 +4593,22 @@ chunkline_remove_trailcomment(sldns_buff
>  {
>         size_t end = sldns_buffer_position(buf);
>         size_t i;
> -       int squote = 0, dquote = 0;
> +       int dquote = 0;
> +       char prev_c = 0;
>         for(i=start; i<end; i++) {
>                 char c = (char)sldns_buffer_read_u8_at(buf, i);
> -               if(squote && c != '\'') continue;
> -               if(dquote && c != '"') continue;
> -               if(c == '"')
> +               if(dquote && !(c == '"' && prev_c != '\\')) {
> +                       prev_c = (prev_c == '\\' && c == '\\') ? 0 : c;
> +                       continue;
> +               }
> +               if(c == '"' && prev_c != '\\')
>                         dquote = !dquote; /* skip quoted part */
> -               else if(c == '\'')
> -                       squote = !squote; /* skip quoted part */
> -               else if(c == ';') {
> +               else if(c == ';' && prev_c != '\\') {
>                         /* rest is a comment */
>                         sldns_buffer_set_position(buf, i);
>                         return;
>                 }
> +               prev_c = (prev_c == '\\' && c == '\\') ? 0 : c;
>         }
>         /* nothing to remove */
>  }
> @@ -4932,6 +5032,8 @@ apply_ixfr(struct auth_xfer* xfr, struct
>         int delmode = 0;
>         int softfail = 0;
>
> +       xfr->num_ixfrs++;
> +
>         /* start RR iterator over chunklist of packets */
>         chunk_rrlist_start(xfr, &rr_chunk, &rr_num, &rr_pos);
>         while(!chunk_rrlist_end(rr_chunk, rr_num)) {
> @@ -5067,16 +5169,11 @@ apply_axfr(struct auth_xfer* xfr, struct
>         size_t rr_counter = 0;
>         int have_end_soa = 0;
>
> -       /* clear the data tree */
> -       traverse_postorder(&z->data, auth_data_del, NULL);
> -       rbtree_init(&z->data, &auth_data_cmp);
> -       /* clear the RPZ policies */
> -       if(z->rpz)
> -               rpz_clear(z->rpz);
> -
> +       auth_zone_clear_data(z);
>         xfr->have_zone = 0;
>         xfr->serial = 0;
>         xfr->soa_zone_acquired = 0;
> +       xfr->num_ixfrs = 0;
>
>         /* insert all RRs in to the zone */
>         /* insert the SOA only once, skip the last one */
> @@ -5169,16 +5266,11 @@ apply_http(struct auth_xfer* xfr, struct
>                 return 0;
>         }
>
> -       /* clear the data tree */
> -       traverse_postorder(&z->data, auth_data_del, NULL);
> -       rbtree_init(&z->data, &auth_data_cmp);
> -       /* clear the RPZ policies */
> -       if(z->rpz)
> -               rpz_clear(z->rpz);
> -
> +       auth_zone_clear_data(z);
>         xfr->have_zone = 0;
>         xfr->serial = 0;
>         xfr->soa_zone_acquired = 0;
> +       xfr->num_ixfrs = 0;
>
>         chunk = xfr->task_transfer->chunks_first;
>         chunk_pos = 0;
> @@ -5224,7 +5316,7 @@ apply_http(struct auth_xfer* xfr, struct
>
>  /** write http chunks to zonefile to create downloaded file */
>  static int
> -auth_zone_write_chunks(struct auth_xfer* xfr, const char* fname)
> +auth_zone_write_chunks(struct auth_chunk* chunk_list, const char* fname)
>  {
>         FILE* out;
>         struct auth_chunk* p;
> @@ -5233,7 +5325,7 @@ auth_zone_write_chunks(struct auth_xfer*
>                 log_err("could not open %s: %s", fname, strerror(errno));
>                 return 0;
>         }
> -       for(p = xfr->task_transfer->chunks_first; p ; p = p->next) {
> +       for(p = chunk_list; p ; p = p->next) {
>                 if(!write_out(out, (char*)p->data, p->len)) {
>                         log_err("could not write http download to %s",
> fname);
>                         fclose(out);
> @@ -5244,34 +5336,18 @@ auth_zone_write_chunks(struct auth_xfer*
>         return 1;
>  }
>
> -/** write to zonefile after zone has been updated */
> +/** write to zonefile after zone has been updated, z has rdlock by
> caller. */
>  static void
> -xfr_write_after_update(struct auth_xfer* xfr, struct module_env* env)
> +zone_write_after_update(struct auth_zone* z, struct module_env* env,
> +       struct auth_chunk* chunk_list)
>  {
>         struct config_file* cfg = env->cfg;
> -       struct auth_zone* z;
>         char tmpfile[1024];
>         char* zfilename;
> -       lock_basic_unlock(&xfr->lock);
> -
> -       /* get lock again, so it is a readlock and concurrently queries
> -        * can be answered */
> -       lock_rw_rdlock(&env->auth_zones->lock);
> -       z = auth_zone_find(env->auth_zones, xfr->name, xfr->namelen,
> -               xfr->dclass);
> -       if(!z) {
> -               lock_rw_unlock(&env->auth_zones->lock);
> -               /* the zone is gone, ignore xfr results */
> -               lock_basic_lock(&xfr->lock);
> -               return;
> -       }
> -       lock_rw_rdlock(&z->lock);
> -       lock_basic_lock(&xfr->lock);
> -       lock_rw_unlock(&env->auth_zones->lock);
>
>         if(z->zonefile == NULL || z->zonefile[0] == 0) {
> -               lock_rw_unlock(&z->lock);
>                 /* no write needed, no zonefile set */
> +               auth_chunk_list_delete(chunk_list);
>                 return;
>         }
>         zfilename = z->zonefile;
> @@ -5288,21 +5364,21 @@ xfr_write_after_update(struct auth_xfer*
>         if((size_t)strlen(zfilename) + 16 > sizeof(tmpfile)) {
>                 verbose(VERB_ALGO, "tmpfilename too long, cannot update "
>                         " zonefile %s", zfilename);
> -               lock_rw_unlock(&z->lock);
> +               auth_chunk_list_delete(chunk_list);
>                 return;
>         }
>         snprintf(tmpfile, sizeof(tmpfile), "%s.tmp%u", zfilename,
>                 (unsigned)getpid());
> -       if(xfr->task_transfer->master->http) {
> +       if(chunk_list) {
>                 /* use the stored chunk list to write them */
> -               if(!auth_zone_write_chunks(xfr, tmpfile)) {
> +               if(!auth_zone_write_chunks(chunk_list, tmpfile)) {
>                         unlink(tmpfile);
> -                       lock_rw_unlock(&z->lock);
> +                       auth_chunk_list_delete(chunk_list);
>                         return;
>                 }
> +               auth_chunk_list_delete(chunk_list);
>         } else if(!auth_zone_write_file(z, tmpfile)) {
>                 unlink(tmpfile);
> -               lock_rw_unlock(&z->lock);
>                 return;
>         }
>  #ifdef UB_ON_WINDOWS
> @@ -5312,9 +5388,57 @@ xfr_write_after_update(struct auth_xfer*
>                 log_err("could not rename(%s, %s): %s", tmpfile, zfilename,
>                         strerror(errno));
>                 unlink(tmpfile);
> -               lock_rw_unlock(&z->lock);
>                 return;
>         }
> +}
> +
> +/** write to zonefile after zone has updated, reacquires z readlock. */
> +static void
> +zone_write_after_update_reacq(uint8_t* bakname, size_t baknamelen,
> +       uint16_t bakdclass, struct module_env* env,
> +       struct auth_chunk* chunk_list)
> +{
> +       struct auth_zone* z;
> +       /* get lock again, so it is a readlock and concurrently queries
> +        * can be answered */
> +       lock_rw_rdlock(&env->auth_zones->lock);
> +       z = auth_zone_find(env->auth_zones, bakname, baknamelen,
> bakdclass);
> +       if(!z) {
> +               lock_rw_unlock(&env->auth_zones->lock);
> +               /* the zone is gone, ignore xfr results */
> +               return;
> +       }
> +       lock_rw_rdlock(&z->lock);
> +       lock_rw_unlock(&env->auth_zones->lock);
> +
> +       zone_write_after_update(z, env, chunk_list);
> +       lock_rw_unlock(&z->lock);
> +}
> +
> +/** write to zonefile after zone has been updated */
> +static void
> +xfr_write_after_update(struct auth_xfer* xfr, struct module_env* env,
> +       struct auth_chunk* chunk_list)
> +{
> +       struct auth_zone* z;
> +       lock_basic_unlock(&xfr->lock);
> +
> +       /* get lock again, so it is a readlock and concurrently queries
> +        * can be answered */
> +       lock_rw_rdlock(&env->auth_zones->lock);
> +       z = auth_zone_find(env->auth_zones, xfr->name, xfr->namelen,
> +               xfr->dclass);
> +       if(!z) {
> +               lock_rw_unlock(&env->auth_zones->lock);
> +               /* the zone is gone, ignore xfr results */
> +               lock_basic_lock(&xfr->lock);
> +               return;
> +       }
> +       lock_rw_rdlock(&z->lock);
> +       lock_basic_lock(&xfr->lock);
> +       lock_rw_unlock(&env->auth_zones->lock);
> +
> +       zone_write_after_update(z, env, chunk_list);
>         lock_rw_unlock(&z->lock);
>  }
>
> @@ -5347,6 +5471,8 @@ xfr_process_chunk_list(struct auth_xfer*
>         int* ixfr_fail)
>  {
>         struct auth_zone* z;
> +       int zonemd_in_progress;
> +       struct auth_chunk* current_chunk_list = NULL;
>
>         /* obtain locks and structures */
>         lock_basic_unlock(&xfr->lock);
> @@ -5359,6 +5485,7 @@ xfr_process_chunk_list(struct auth_xfer*
>         /* apply data */
>         if(xfr->task_transfer->master->http) {
>                 if(!apply_http(xfr, z, env->scratch_buffer)) {
> +                       auth_zone_clear_data(z);
>                         lock_rw_unlock(&z->lock);
>                         verbose(VERB_ALGO, "http from %s: could not store
> data",
>                                 xfr->task_transfer->master->host);
> @@ -5367,6 +5494,7 @@ xfr_process_chunk_list(struct auth_xfer*
>         } else if(xfr->task_transfer->on_ixfr &&
>                 !xfr->task_transfer->on_ixfr_is_axfr) {
>                 if(!apply_ixfr(xfr, z, env->scratch_buffer)) {
> +                       auth_zone_clear_data(z);
>                         lock_rw_unlock(&z->lock);
>                         verbose(VERB_ALGO, "xfr from %s: could not store
> IXFR"
>                                 " data", xfr->task_transfer->master->host);
> @@ -5375,6 +5503,7 @@ xfr_process_chunk_list(struct auth_xfer*
>                 }
>         } else {
>                 if(!apply_axfr(xfr, z, env->scratch_buffer)) {
> +                       auth_zone_clear_data(z);
>                         lock_rw_unlock(&z->lock);
>                         verbose(VERB_ALGO, "xfr from %s: could not store
> AXFR"
>                                 " data", xfr->task_transfer->master->host);
> @@ -5391,6 +5520,7 @@ xfr_process_chunk_list(struct auth_xfer*
>         }
>         z->soa_zone_acquired = *env->now;
>         xfr->soa_zone_acquired = *env->now;
> +       xfr->is_rpz = (z->rpz!=NULL);
>
>         /* release xfr lock while verifying zonemd because it may have
>          * to spawn lookups in the state machines */
> @@ -5426,6 +5556,25 @@ xfr_process_chunk_list(struct auth_xfer*
>         if(z->rpz)
>                 rpz_finish_config(z->rpz);
>
> +       if(z->zonemd_check && z->zonemd_callback_env) {
> +               zonemd_in_progress = 1;
> +               z->zonemd_callback_perform_write = 1;
> +               auth_chunk_list_delete(z->perform_write_chunk_list);
> +               z->perform_write_chunk_list = NULL;
> +               if(xfr->task_transfer->master->http) {
> +                       z->perform_write_chunk_list =
> xfr->task_transfer->chunks_first;
> +                       xfr->task_transfer->chunks_first = NULL;
> +                       auth_chunks_delete(xfr->task_transfer);
> +               }
> +       } else {
> +               zonemd_in_progress = 0;
> +               z->zonemd_callback_perform_write = 0;
> +               if(xfr->task_transfer->master->http) {
> +                       current_chunk_list =
> xfr->task_transfer->chunks_first;
> +                       xfr->task_transfer->chunks_first = NULL;
> +                       auth_chunks_delete(xfr->task_transfer);
> +               }
> +       }
>         /* unlock */
>         lock_rw_unlock(&z->lock);
>
> @@ -5436,20 +5585,56 @@ xfr_process_chunk_list(struct auth_xfer*
>                         (unsigned)xfr->serial);
>         }
>         /* see if we need to write to a zonefile */
> -       xfr_write_after_update(xfr, env);
> +       if(!zonemd_in_progress) {
> +               xfr_write_after_update(xfr, env, current_chunk_list);
> +       }
>         return 1;
>  }
>
> +/** Stop lookup using callback */
> +static void
> +xfr_stop_lookup(struct auth_master** lookup_target, void*
> lookup_unique_info,
> +       int lookup_aaaa, uint16_t dclass, struct mesh_area* mesh,
> +       mesh_cb_func_type cb, void* cb_arg)
> +{
> +       struct query_info qinfo;
> +       uint8_t dname[LDNS_MAX_DOMAINLEN+1];
> +       if(!*lookup_target) return;
> +       qinfo.qname_len = sizeof(dname);
> +       if(sldns_str2wire_dname_buf((*lookup_target)->host, dname,
> +               &qinfo.qname_len) != 0) {
> +               *lookup_target = NULL;
> +               return;
> +       }
> +       qinfo.qname = dname;
> +       qinfo.qclass = dclass;
> +       qinfo.qtype = lookup_aaaa ? LDNS_RR_TYPE_AAAA : LDNS_RR_TYPE_A;
> +       qinfo.local_alias = NULL;
> +       log_query_info(VERB_ALGO, "removing xfr callback", &qinfo);
> +
> +       mesh_remove_callback(mesh, &qinfo, BIT_RD, cb, cb_arg,
> +               lookup_unique_info);
> +       *lookup_target = NULL;
> +}
> +
>  /** disown task_transfer.  caller must hold xfr.lock */
>  static void
>  xfr_transfer_disown(struct auth_xfer* xfr)
>  {
> +       /* remove data chunks */
> +       auth_chunks_delete(xfr->task_transfer);
>         /* remove timer (from this worker's event base) */
>         comm_timer_delete(xfr->task_transfer->timer);
>         xfr->task_transfer->timer = NULL;
>         /* remove the commpoint */
>         comm_point_delete(xfr->task_transfer->cp);
>         xfr->task_transfer->cp = NULL;
> +       if(xfr->task_transfer->env)
> +               xfr_stop_lookup(&xfr->task_transfer->lookup_target,
> +                       xfr->task_transfer->lookup_unique_info,
> +                       xfr->task_transfer->lookup_aaaa, xfr->dclass,
> +                       xfr->task_transfer->env->mesh,
> +                       &auth_xfer_transfer_lookup_callback, xfr);
>         /* we don't own this item anymore */
>         xfr->task_transfer->worker = NULL;
>         xfr->task_transfer->env = NULL;
> @@ -5516,7 +5701,8 @@ xfr_transfer_lookup_host(struct auth_xfe
>          * called straight away */
>         lock_basic_unlock(&xfr->lock);
>         if(!mesh_new_callback(env->mesh, &qinfo, qflags, &edns, buf, 0,
> -               &auth_xfer_transfer_lookup_callback, xfr, 0)) {
> +               &auth_xfer_transfer_lookup_callback, xfr, 0,
> +               &xfr->task_transfer->lookup_unique_info)) {
>                 lock_basic_lock(&xfr->lock);
>                 log_err("out of memory lookup up master %s", master->host);
>                 return 0;
> @@ -5574,6 +5760,7 @@ xfr_transfer_init_fetch(struct auth_xfer
>          t.tv_sec = timeout/1000;
>          t.tv_usec = (timeout%1000)*1000;
>  #endif
> +       xfr->task_transfer->start_time = *env->now_tv;
>
>         if(master->http) {
>                 /* perform http fetch */
> @@ -5743,6 +5930,31 @@ xfr_master_add_addrs(struct auth_master*
>         }
>  }
>
> +/** check if the lookup target name equals the found answer name. */
> +static int
> +xfer_target_equals_answer_name(struct auth_master* lookup_target,
> +       struct ub_packed_rrset_key* answer, struct query_info* rq,
> +       struct reply_info* rep)
> +{
> +       uint8_t qname[LDNS_MAX_DOMAINLEN+1];
> +       size_t qname_len;
> +       if(!lookup_target) return 0;
> +       if(!answer) return 0;
> +       qname_len = sizeof(qname);
> +       if(sldns_str2wire_dname_buf(lookup_target->host, qname, &qname_len)
> +               != 0) {
> +               verbose(VERB_ALGO, "xfer_target_equals_answer_name: could
> not parse auth host name");
> +               return 0;
> +       }
> +       if(query_dname_compare(answer->rk.dname, qname) == 0)
> +               return 1;
> +       /* It could be a CNAME. */
> +       if(reply_find_rrset_section_an(rep, qname, qname_len,
> +               LDNS_RR_TYPE_CNAME, rq->qclass))
> +               return 1;
> +       return 0;
> +}
> +
>  /** callback for task_transfer lookup of host name, of A or AAAA */
>  void auth_xfer_transfer_lookup_callback(void* arg, int rcode,
> sldns_buffer* buf,
>         enum sec_status sec, char* why_bogus, int
> ATTR_UNUSED(was_ratelimited))
> @@ -5781,21 +5993,29 @@ void auth_xfer_transfer_lookup_callback(
>                         /* parsed successfully */
>                         struct ub_packed_rrset_key* answer =
>                                 reply_find_answer_rrset(&rq, rep);
> -                       if(answer) {
> +                       if(answer && xfer_target_equals_answer_name(
> +                               xfr->task_transfer->lookup_target, answer,
> +                               &rq, rep)) {
>                                 xfr_master_add_addrs(xfr->task_transfer->
>                                         lookup_target, answer,
> wanted_qtype);
> +                       } else if(answer) {
> +                               if(verbosity >= VERB_ALGO) {
> +                                       char zname[LDNS_MAX_DOMAINLEN];
> +                                       dname_str(xfr->name, zname);
> +                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s transfer lookup has mismatch in answer name", zname,
> ((xfr->task_transfer->lookup_target &&
> xfr->task_transfer->lookup_target->host) ?
> xfr->task_transfer->lookup_target->host : "null"),
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
> +                               }
>                         } else {
>                                 if(verbosity >= VERB_ALGO) {
>                                         char zname[LDNS_MAX_DOMAINLEN];
>                                         dname_str(xfr->name, zname);
> -                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s transfer lookup has nodata", zname,
> xfr->task_transfer->lookup_target->host,
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
> +                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s transfer lookup has nodata", zname,
> ((xfr->task_transfer->lookup_target &&
> xfr->task_transfer->lookup_target->host) ?
> xfr->task_transfer->lookup_target->host : "null"),
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
>                                 }
>                         }
>                 } else {
>                         if(verbosity >= VERB_ALGO) {
>                                 char zname[LDNS_MAX_DOMAINLEN];
>                                 dname_str(xfr->name, zname);
> -                               verbose(VERB_ALGO, "auth zone %s host %s
> type %s transfer lookup has no answer", zname,
> xfr->task_transfer->lookup_target->host,
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
> +                               verbose(VERB_ALGO, "auth zone %s host %s
> type %s transfer lookup has no answer", zname,
> ((xfr->task_transfer->lookup_target &&
> xfr->task_transfer->lookup_target->host) ?
> xfr->task_transfer->lookup_target->host : "null"),
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
>                         }
>                 }
>                 regional_free_all(temp);
> @@ -5803,10 +6023,11 @@ void auth_xfer_transfer_lookup_callback(
>                 if(verbosity >= VERB_ALGO) {
>                         char zname[LDNS_MAX_DOMAINLEN];
>                         dname_str(xfr->name, zname);
> -                       verbose(VERB_ALGO, "auth zone %s host %s type %s
> transfer lookup failed", zname, xfr->task_transfer->lookup_target->host,
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
> +                       verbose(VERB_ALGO, "auth zone %s host %s type %s
> transfer lookup failed", zname, ((xfr->task_transfer->lookup_target &&
> xfr->task_transfer->lookup_target->host) ?
> xfr->task_transfer->lookup_target->host : "null"),
> (xfr->task_transfer->lookup_aaaa?"AAAA":"A"));
>                 }
>         }
> -       if(xfr->task_transfer->lookup_target->list &&
> +       if(xfr->task_transfer->lookup_target &&
> +               xfr->task_transfer->lookup_target->list &&
>                 xfr->task_transfer->lookup_target ==
> xfr_transfer_current_master(xfr))
>                 xfr->task_transfer->scan_addr =
> xfr->task_transfer->lookup_target->list;
>
> @@ -6136,6 +6357,7 @@ xfer_link_data(sldns_buffer* pkt, struct
>         if(xfr->task_transfer->chunks_last)
>                 xfr->task_transfer->chunks_last->next = e;
>         xfr->task_transfer->chunks_last = e;
> +       xfr->task_transfer->chunks_total += e->len;
>         return 1;
>  }
>
> @@ -6231,6 +6453,15 @@ auth_xfer_transfer_timer_callback(void*
>         xfr_transfer_nexttarget_or_end(xfr, env);
>  }
>
> +/** return the time taken by the transfer */
> +static int
> +auth_xfer_transfer_time_taken(struct auth_xfer* xfr, struct module_env*
> env)
> +{
> +       struct timeval delta;
> +       timeval_subtract(&delta, env->now_tv,
> &xfr->task_transfer->start_time);
> +       return ((int)delta.tv_sec)*1000 + ((int)delta.tv_usec)/1000;
> +}
> +
>  /** callback for task_transfer tcp connections */
>  int
>  auth_xfer_transfer_tcp_callback(struct comm_point* c, void* arg, int err,
> @@ -6297,6 +6528,15 @@ auth_xfer_transfer_tcp_callback(struct c
>                         xfr->task_transfer->master->host);
>                 goto failed;
>         }
> +       if(xfr->max_transfer_size > 0 &&
> +               xfr->task_transfer->chunks_total > xfr->max_transfer_size)
> {
> +               char zname[LDNS_MAX_DOMAINLEN];
> +               dname_str(xfr->name, zname);
> +               log_err("auth zone %s transfer from %s exceeded %u bytes,
> aborting",
> +                       zname, xfr->task_transfer->master->host,
> +                       (unsigned)xfr->max_transfer_size);
> +               goto failed;
> +       }
>         /* if the transfer is done now, disconnect and process the list */
>         if(transferdone) {
>                 comm_point_delete(xfr->task_transfer->cp);
> @@ -6305,6 +6545,16 @@ auth_xfer_transfer_tcp_callback(struct c
>                 return 0;
>         }
>
> +       if(xfr->max_transfer_time > 0 &&
> +               auth_xfer_transfer_time_taken(xfr, env) >
> xfr->max_transfer_time) {
> +               char zname[LDNS_MAX_DOMAINLEN];
> +               dname_str(xfr->name, zname);
> +               log_err("auth zone %s transfer from %s exceeded %u msec
> total running time, aborting",
> +                       zname, xfr->task_transfer->master->host,
> +                       (unsigned)xfr->max_transfer_time);
> +               goto failed;
> +       }
> +
>         /* if we want to read more messages, setup the commpoint to read
>          * a DNS packet, and the timeout */
>         lock_basic_unlock(&xfr->lock);
> @@ -6360,6 +6610,16 @@ auth_xfer_transfer_http_callback(struct
>                                 xfr->task_transfer->master->host);
>                         goto failed;
>                 }
> +               if(xfr->max_transfer_size > 0 &&
> +                       xfr->task_transfer->chunks_total >
> xfr->max_transfer_size) {
> +                       char zname[LDNS_MAX_DOMAINLEN];
> +                       dname_str(xfr->name, zname);
> +                       log_err("auth zone %s http %s/%s exceeded %u
> bytes, aborting",
> +                               zname, xfr->task_transfer->master->host,
> +                               xfr->task_transfer->master->file,
> +                               (unsigned)xfr->max_transfer_size);
> +                       goto failed;
> +               }
>         }
>         /* if the transfer is done now, disconnect and process the list */
>         if(err == NETEVENT_DONE) {
> @@ -6371,6 +6631,17 @@ auth_xfer_transfer_http_callback(struct
>                 return 0;
>         }
>
> +       if(xfr->max_transfer_time > 0 &&
> +               auth_xfer_transfer_time_taken(xfr, env) >
> xfr->max_transfer_time) {
> +               char zname[LDNS_MAX_DOMAINLEN];
> +               dname_str(xfr->name, zname);
> +               log_err("auth zone %s transfer http %s/%s exceeded %u msec
> total running time, aborting",
> +                       zname, xfr->task_transfer->master->host,
> +                       xfr->task_transfer->master->file,
> +                       (unsigned)xfr->max_transfer_time);
> +               goto failed;
> +       }
> +
>         /* if we want to read more messages, setup the commpoint to read
>          * a DNS packet, and the timeout */
>         lock_basic_unlock(&xfr->lock);
> @@ -6413,6 +6684,12 @@ xfr_probe_disown(struct auth_xfer* xfr)
>         /* remove the commpoint */
>         comm_point_delete(xfr->task_probe->cp);
>         xfr->task_probe->cp = NULL;
> +       if(xfr->task_probe->env)
> +               xfr_stop_lookup(&xfr->task_probe->lookup_target,
> +                       xfr->task_probe->lookup_unique_info,
> +                       xfr->task_probe->lookup_aaaa, xfr->dclass,
> +                       xfr->task_probe->env->mesh,
> +                       &auth_xfer_probe_lookup_callback, xfr);
>         /* we don't own this item anymore */
>         xfr->task_probe->worker = NULL;
>         xfr->task_probe->env = NULL;
> @@ -6719,7 +6996,8 @@ xfr_probe_lookup_host(struct auth_xfer*
>          * called straight away */
>         lock_basic_unlock(&xfr->lock);
>         if(!mesh_new_callback(env->mesh, &qinfo, qflags, &edns, buf, 0,
> -               &auth_xfer_probe_lookup_callback, xfr, 0)) {
> +               &auth_xfer_probe_lookup_callback, xfr, 0,
> +               &xfr->task_probe->lookup_unique_info)) {
>                 lock_basic_lock(&xfr->lock);
>                 log_err("out of memory lookup up master %s", master->host);
>                 return 0;
> @@ -6856,7 +7134,7 @@ void auth_xfer_probe_lookup_callback(voi
>                         char zname[LDNS_MAX_DOMAINLEN];
>                         dname_str(xfr->name, zname);
>                         verbose(VERB_OPS, "auth zone %s: primary %s
> address probe lookup is DNSSEC bogus: %s",
> -                               zname,
> xfr->task_transfer->lookup_target->host,
> +                               zname,
> xfr->task_probe->lookup_target->host,
>                                 (why_bogus?why_bogus:""));
>                 }
>                 /* fall through to next-lookup / next-master */
> @@ -6874,21 +7152,29 @@ void auth_xfer_probe_lookup_callback(voi
>                         /* parsed successfully */
>                         struct ub_packed_rrset_key* answer =
>                                 reply_find_answer_rrset(&rq, rep);
> -                       if(answer) {
> +                       if(answer && xfer_target_equals_answer_name(
> +                               xfr->task_probe->lookup_target, answer,
> +                               &rq, rep)) {
>                                 xfr_master_add_addrs(xfr->task_probe->
>                                         lookup_target, answer,
> wanted_qtype);
> +                       } else if(answer) {
> +                               if(verbosity >= VERB_ALGO) {
> +                                       char zname[LDNS_MAX_DOMAINLEN];
> +                                       dname_str(xfr->name, zname);
> +                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s probe lookup has mismatch in answer name", zname,
> ((xfr->task_probe->lookup_target && xfr->task_probe->lookup_target->host) ?
> xfr->task_probe->lookup_target->host : "null"),
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
> +                               }
>                         } else {
>                                 if(verbosity >= VERB_ALGO) {
>                                         char zname[LDNS_MAX_DOMAINLEN];
>                                         dname_str(xfr->name, zname);
> -                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s probe lookup has nodata", zname,
> xfr->task_probe->lookup_target->host,
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
> +                                       verbose(VERB_ALGO, "auth zone %s
> host %s type %s probe lookup has nodata", zname,
> ((xfr->task_probe->lookup_target && xfr->task_probe->lookup_target->host) ?
> xfr->task_probe->lookup_target->host : "null"),
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
>                                 }
>                         }
>                 } else {
>                         if(verbosity >= VERB_ALGO) {
>                                 char zname[LDNS_MAX_DOMAINLEN];
>                                 dname_str(xfr->name, zname);
> -                               verbose(VERB_ALGO, "auth zone %s host %s
> type %s probe lookup has no address", zname,
> xfr->task_probe->lookup_target->host,
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
> +                               verbose(VERB_ALGO, "auth zone %s host %s
> type %s probe lookup has no address", zname,
> ((xfr->task_probe->lookup_target && xfr->task_probe->lookup_target->host) ?
> xfr->task_probe->lookup_target->host : "null"),
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
>                         }
>                 }
>                 regional_free_all(temp);
> @@ -6896,10 +7182,11 @@ void auth_xfer_probe_lookup_callback(voi
>                 if(verbosity >= VERB_ALGO) {
>                         char zname[LDNS_MAX_DOMAINLEN];
>                         dname_str(xfr->name, zname);
> -                       verbose(VERB_ALGO, "auth zone %s host %s type %s
> probe lookup failed", zname, xfr->task_probe->lookup_target->host,
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
> +                       verbose(VERB_ALGO, "auth zone %s host %s type %s
> probe lookup failed", zname, ((xfr->task_probe->lookup_target &&
> xfr->task_probe->lookup_target->host) ?
> xfr->task_probe->lookup_target->host : "null"),
> (xfr->task_probe->lookup_aaaa?"AAAA":"A"));
>                 }
>         }
> -       if(xfr->task_probe->lookup_target->list &&
> +       if(xfr->task_probe->lookup_target &&
> +               xfr->task_probe->lookup_target->list &&
>                 xfr->task_probe->lookup_target ==
> xfr_probe_current_master(xfr))
>                 xfr->task_probe->scan_addr =
> xfr->task_probe->lookup_target->list;
>
> @@ -6966,8 +7253,8 @@ xfr_start_probe(struct auth_xfer* xfr, s
>                 if(!have_probe_targets(xfr->task_probe->masters) &&
>                         xfr->task_probe->masters != NULL)
>                         xfr->task_probe->only_lookup = 1;
> -               if(!(xfr->task_probe->only_lookup &&
> -                       xfr->task_probe->masters != NULL)) {
> +               if(!xfr->task_probe->only_lookup &&
> +                       !have_probe_targets(xfr->task_probe->masters)) {
>                         /* useless to pick up task_probe, no masters to
>                          * probe. Instead attempt to pick up task transfer
> */
>                         if(xfr->task_transfer->worker == NULL) {
> @@ -7170,6 +7457,8 @@ auth_xfer_new(struct auth_zone* z)
>         xfr->namelen = z->namelen;
>         xfr->namelabs = z->namelabs;
>         xfr->dclass = z->dclass;
> +       xfr->max_transfer_size = z->max_transfer_size;
> +       xfr->max_transfer_time = z->max_transfer_time;
>
>         xfr->task_nextprobe = (struct auth_nextprobe*)calloc(1,
>                 sizeof(struct auth_nextprobe));
> @@ -7379,35 +7668,48 @@ xfer_set_masters(struct auth_master** li
>  {
>         struct auth_master* m;
>         struct config_strlist* p;
> +       struct auth_master** tail;
>         /* list points to the first, or next pointer for the new element */
>         while(*list) {
>                 list = &( (*list)->next );
>         }
>         if(with_http)
>           for(p = c->urls; p; p = p->next) {
> +               tail = list;
>                 m = auth_master_new(&list);
>                 if(!m) return 0;
>                 m->http = 1;
> -               if(!parse_url(p->str, &m->host, &m->file, &m->port,
> &m->ssl))
> +               if(!parse_url(p->str, &m->host, &m->file, &m->port,
> &m->ssl)) {
> +                       free(m->host);
> +                       free(m->file);
> +                       free(m);
> +                       *tail = NULL;
>                         return 0;
> +               }
>         }
>         for(p = c->masters; p; p = p->next) {
> +               tail = list;
>                 m = auth_master_new(&list);
>                 if(!m) return 0;
>                 m->ixfr = 1; /* this flag is not configurable */
>                 m->host = strdup(p->str);
>                 if(!m->host) {
>                         log_err("malloc failure");
> +                       free(m);
> +                       *tail = NULL;
>                         return 0;
>                 }
>         }
>         for(p = c->allow_notify; p; p = p->next) {
> +               tail = list;
>                 m = auth_master_new(&list);
>                 if(!m) return 0;
>                 m->allow_notify = 1;
>                 m->host = strdup(p->str);
>                 if(!m->host) {
>                         log_err("malloc failure");
> +                       free(m);
> +                       *tail = NULL;
>                         return 0;
>                 }
>         }
> @@ -7932,7 +8234,8 @@ static int zonemd_dnssec_verify_rrset(st
>                         "zonemd: verify %s RRset with DNSKEY", typestr);
>         }
>         sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg,
> why_bogus, NULL,
> -               LDNS_SECTION_ANSWER, NULL, &verified, reasonbuf,
> reasonlen);
> +               LDNS_SECTION_ANSWER, NULL, NULL, &verified, reasonbuf,
> +               reasonlen);
>         if(sec == sec_status_secure) {
>                 return 1;
>         }
> @@ -8281,8 +8584,8 @@ zonemd_get_dnskey_from_anchor(struct aut
>         auth_zone_log(z->name, VERB_QUERY,
>                 "zonemd: verify DNSKEY RRset with trust anchor");
>         sec = val_verify_DNSKEY_with_TA(env, ve, keystorage,
> anchor->ds_rrset,
> -               anchor->dnskey_rrset, NULL, why_bogus, NULL, NULL,
> reasonbuf,
> -               reasonlen);
> +               anchor->dnskey_rrset, NULL, why_bogus, NULL, NULL, NULL,
> +               reasonbuf, reasonlen);
>         regional_free_all(env->scratch);
>         if(sec == sec_status_secure) {
>                 /* success */
> @@ -8342,7 +8645,7 @@ auth_zone_verify_zonemd_key_with_ds(stru
>         keystorage->rk.rrset_class = htons(z->dclass);
>         auth_zone_log(z->name, VERB_QUERY, "zonemd: verify zone DNSKEY
> with DS");
>         sec = val_verify_DNSKEY_with_DS(env, ve, keystorage, ds, sigalg,
> -               why_bogus, NULL, NULL, reasonbuf, reasonlen);
> +               why_bogus, NULL, NULL, NULL, reasonbuf, reasonlen);
>         regional_free_all(env->scratch);
>         if(sec == sec_status_secure) {
>                 /* success */
> @@ -8371,9 +8674,13 @@ void auth_zonemd_dnskey_lookup_callback(
>         char reasonbuf[256];
>         char* reason = NULL, *ds_bogus = NULL, *typestr="DNSKEY";
>         struct ub_packed_rrset_key* dnskey = NULL, *ds = NULL;
> -       int is_insecure = 0, downprot;
> +       int is_insecure = 0, downprot, perform_write = 0;
>         struct ub_packed_rrset_key keystorage;
>         uint8_t sigalg[ALGO_NEEDS_MAX+1];
> +       uint8_t bakname[LDNS_MAX_DOMAINLEN];
> +       size_t baknamelen;
> +       uint16_t bakdclass;
> +       struct auth_chunk* chunk_list = NULL;
>
>         lock_rw_wrlock(&z->lock);
>         env = z->zonemd_callback_env;
> @@ -8496,7 +8803,37 @@ void auth_zonemd_dnskey_lookup_callback(
>         auth_zone_verify_zonemd_with_key(z, env, &env->mesh->mods, dnskey,
>                 is_insecure, NULL, downprot?sigalg:NULL);
>         regional_free_all(env->scratch);
> +
> +       if(z->zonemd_callback_perform_write) {
> +               if(!z->zone_expired) {
> +                       /* Write to zonefile if the ZONEMD is okay. */
> +                       perform_write = 1;
> +                       /* copy the key to lookup the z structure.
> +                        * The new lookup is readonly so concurrent
> +                        * queries can continue. */
> +                       if(z->namelen > sizeof(bakname)) {
> +                               perform_write = 0;
> +
>  auth_chunk_list_delete(z->perform_write_chunk_list);
> +                               z->perform_write_chunk_list = NULL;
> +                       } else {
> +                               memcpy(bakname, z->name, z->namelen);
> +                               baknamelen = z->namelen;
> +                               bakdclass = z->dclass;
> +                               chunk_list = z->perform_write_chunk_list;
> +                               z->perform_write_chunk_list = NULL;
> +                       }
> +               } else {
> +
>  auth_chunk_list_delete(z->perform_write_chunk_list);
> +                       z->perform_write_chunk_list = NULL;
> +               }
> +               z->zonemd_callback_perform_write = 0;
> +       }
>         lock_rw_unlock(&z->lock);
> +
> +       if(perform_write) {
> +               zone_write_after_update_reacq(bakname, baknamelen,
> bakdclass,
> +                       env, chunk_list);
> +       }
>  }
>
>  /** lookup DNSKEY for ZONEMD verification */
> @@ -8561,8 +8898,12 @@ zonemd_lookup_dnskey(struct auth_zone* z
>         /* the callback can be called straight away */
>         lock_rw_unlock(&z->lock);
>         if(!mesh_new_callback(env->mesh, &qinfo, qflags, &edns, buf, 0,
> -               &auth_zonemd_dnskey_lookup_callback, z, 0)) {
> +               &auth_zonemd_dnskey_lookup_callback, z, 0,
> +               &z->zonemd_callback_unique_info)) {
>                 lock_rw_wrlock(&z->lock);
> +               /* no callback will run; do not leave the pending
> +                * marker set */
> +               z->zonemd_callback_env = NULL;
>                 log_err("out of memory lookup of %s for zonemd",
>                         (fetch_ds?"DS":"DNSKEY"));
>                 return 0;
> Index: services/authzone.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/authzone.h,v
> diff -u -p -r1.15 authzone.h
> --- services/authzone.h 26 Sep 2025 07:32:37 -0000      1.15
> +++ services/authzone.h 20 Sep 2026 09:50:48 -0000
> @@ -144,6 +144,12 @@ struct auth_zone {
>         struct module_env* zonemd_callback_env;
>         /** for the zonemd callback, the type of data looked up */
>         uint16_t zonemd_callback_qtype;
> +       /** for the zonemd callback, the unique info */
> +       void* zonemd_callback_unique_info;
> +       /** if the zonemd callback should write to file */
> +       int zonemd_callback_perform_write;
> +       /** chunklist to write for chunked transfer. */
> +       struct auth_chunk* perform_write_chunk_list;
>         /** zone has been deleted */
>         int zone_deleted;
>         /** deletelist pointer, unused normally except during delete */
> @@ -153,6 +159,10 @@ struct auth_zone {
>         struct auth_zone* rpz_az_next;
>         /** previous auth zone containing RPZ data, or NULL */
>         struct auth_zone* rpz_az_prev;
> +       /** The maximum auth zone transfer size, in bytes. */
> +       size_t max_transfer_size;
> +       /** The maximum auth zone transfer time taken, in msec. */
> +       int max_transfer_time;
>  };
>
>  /**
> @@ -283,6 +293,15 @@ struct auth_xfer {
>          * this is renewed every SOA probe and transfer.  On zone load
>          * from zonefile it is also set (with probe set soon to check) */
>         time_t lease_time;
> +
> +       /** The maximum auth zone transfer size, in bytes. */
> +       size_t max_transfer_size;
> +       /** The maximum auth zone transfer time taken, in msec. */
> +       int max_transfer_time;
> +       /** the zone is an rpz zone */
> +       int is_rpz;
> +       /** the number of IXFRs since the last full transfer. */
> +       int num_ixfrs;
>  };
>
>  /**
> @@ -331,6 +350,8 @@ struct auth_probe {
>
>         /** for the hostname lookups, which master is current */
>         struct auth_master* lookup_target;
> +       /** for the lookup, the callback unique info */
> +       void* lookup_unique_info;
>         /** are we looking up A or AAAA, first A, then AAAA (if ip6
> enabled) */
>         int lookup_aaaa;
>         /** we only want to do lookups for making config work (for notify),
> @@ -379,12 +400,18 @@ struct auth_transfer {
>         struct auth_chunk* chunks_first;
>         /** last element in chunks list (to append new data at the end) */
>         struct auth_chunk* chunks_last;
> +       /** running total of bytes held in chunks_first..chunks_last */
> +       size_t chunks_total;
> +       /** start time of the transfer */
> +       struct timeval start_time;
>
>         /** list of upstream masters for this zone, from config */
>         struct auth_master* masters;
>
>         /** for the hostname lookups, which master is current */
>         struct auth_master* lookup_target;
> +       /** for the lookup, the callback unique info */
> +       void* lookup_unique_info;
>         /** are we looking up A or AAAA, first A, then AAAA (if ip6
> enabled) */
>         int lookup_aaaa;
>
> @@ -827,5 +854,11 @@ void auth_xfer_delete(struct auth_xfer*
>   * @param worker: the worker for which to stop tasks.
>   */
>  void xfr_disown_tasks(struct auth_xfer* xfr, struct worker* worker);
> +
> +/** count number of open and closed parenthesis in a chunkline */
> +int chunkline_count_parens(struct sldns_buffer* buf, size_t start);
> +
> +/** Clear data in auth zone */
> +void auth_zone_clear_data(struct auth_zone* z);
>
>  #endif /* SERVICES_AUTHZONE_H */
> Index: services/listen_dnsport.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/listen_dnsport.c,v
> diff -u -p -r1.42 listen_dnsport.c
> --- services/listen_dnsport.c   27 Jul 2026 14:14:39 -0000      1.42
> +++ services/listen_dnsport.c   20 Sep 2026 09:50:48 -0000
> @@ -1125,7 +1125,7 @@ make_sock_port(int stype, const char* if
>         int use_systemd, int dscp, struct unbound_socket* ub_sock,
>         const char* additional)
>  {
> -       char* s = strchr(ifname, '@');
> +       const char* s = strchr(ifname, '@');
>         if(s) {
>                 /* override port with ifspec@port */
>                 int port;
> @@ -2133,7 +2133,7 @@ void listen_start_accept(struct listen_d
>  }
>
>  struct tcp_req_info*
> -tcp_req_info_create(struct sldns_buffer* spoolbuf)
> +tcp_req_info_create(struct comm_base* base, struct sldns_buffer* spoolbuf)
>  {
>         struct tcp_req_info* req = (struct
> tcp_req_info*)malloc(sizeof(*req));
>         if(!req) {
> @@ -2141,6 +2141,12 @@ tcp_req_info_create(struct sldns_buffer*
>                 return NULL;
>         }
>         memset(req, 0, sizeof(*req));
> +       req->read_again_timer = comm_timer_create(base, tcp_read_again_cb,
> req);
> +       if(!req->read_again_timer) {
> +               log_err("malloc failure");
> +               free(req);
> +               return NULL;
> +       }
>         req->spool_buffer = spoolbuf;
>         return req;
>  }
> @@ -2150,6 +2156,7 @@ tcp_req_info_delete(struct tcp_req_info*
>  {
>         if(!req) return;
>         tcp_req_info_clear(req);
> +       comm_timer_delete(req->read_again_timer);
>         /* cp is pointer back to commpoint that owns this struct and
>          * called delete on us */
>         /* spool_buffer is shared udp buffer, not deleted here */
> @@ -2167,7 +2174,7 @@ void tcp_req_info_clear(struct tcp_req_i
>         while(open) {
>                 nopen = open->next;
>                 mesh_state_remove_reply(open->mesh, open->mesh_state,
> req->cp,
> -                       NULL);
> +                       NULL, NULL);
>                 free(open);
>                 open = nopen;
>         }
> @@ -2189,6 +2196,9 @@ void tcp_req_info_clear(struct tcp_req_i
>         req->done_req_list = NULL;
>         req->num_done_req = 0;
>         req->read_is_closed = 0;
> +
> +       if(comm_timer_is_set(req->read_again_timer))
> +               comm_timer_disable(req->read_again_timer);
>  }
>
>  void
> @@ -3617,7 +3627,7 @@ stream_tree_del(rbnode_type* node, void*
>         stream = (struct doq_stream*)node;
>         if(stream->mesh_state) {
>                 mesh_state_remove_reply(stream->mesh, stream->mesh_state,
> -                       args->conn->doq_socket->cp, stream);
> +                       args->conn->doq_socket->cp, NULL, stream);
>                 stream->mesh_state = NULL;
>         }
>         if(stream->in)
> @@ -3639,7 +3649,8 @@ doq_conn_delete(struct doq_conn* conn, s
>         lock_rw_unlock(&conn->table->conid_lock);
>         /* Remove the app data from ngtcp2 before SSL_free of conn->ssl,
>          * because the ngtcp2 conn is deleted. */
> -       SSL_set_app_data(conn->ssl, NULL);
> +       if(conn->ssl)
> +               SSL_set_app_data(conn->ssl, NULL);
>         if(conn->stream_tree.count != 0) {
>                 struct doq_stream_tree_del_args args;
>                 memset(&args, 0, sizeof(args));
> @@ -3956,7 +3967,7 @@ doq_stream_close(struct doq_conn* conn,
>         stream->is_closed = 1;
>         if(stream->mesh_state) {
>                 mesh_state_remove_reply(stream->mesh, stream->mesh_state,
> -                       conn->doq_socket->cp, stream);
> +                       conn->doq_socket->cp, NULL, stream);
>                 stream->mesh_state = NULL;
>         }
>         doq_stream_off_write_list(conn, stream);
> @@ -4503,7 +4514,7 @@ doq_stream_reset_cb(ngtcp2_conn* ATTR_UN
>                         "unknown stream %d", (int)stream_id);
>                 return 0;
>         }
> -       if(!doq_stream_close(doq_conn, stream, 0))
> +       if(!doq_stream_close(doq_conn, stream, 1))
>                 return NGTCP2_ERR_CALLBACK_FAILURE;
>         return 0;
>  }
> @@ -4851,7 +4862,7 @@ doq_ssl_server_setup(SSL_CTX* ctx, struc
>         SSL_set_app_data(ssl, conn);
>  #endif
>         SSL_set_accept_state(ssl);
> -#ifdef USE_NGTCP2_CRYPTO_OSSL
> +#ifdef HAVE_SSL_SET_QUIC_TLS_EARLY_DATA_ENABLED
>         SSL_set_quic_tls_early_data_enabled(ssl, 1);
>  #else
>         SSL_set_quic_early_data_enabled(ssl, 1);
> @@ -4960,6 +4971,7 @@ doq_conn_setup(struct doq_conn* conn, ui
>         rv = ngtcp2_conn_server_new(&conn->conn, &scid_cid, &sv_scid,
> &path,
>                 conn->version, &callbacks, &settings, &params, NULL, conn);
>         if(rv != 0) {
> +               conn->conn = NULL;
>                 lock_rw_unlock(&conn->table->conid_lock);
>                 log_err("ngtcp2_conn_server_new failed: %s",
>                         ngtcp2_strerror(rv));
> Index: services/listen_dnsport.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/listen_dnsport.h,v
> diff -u -p -r1.24 listen_dnsport.h
> --- services/listen_dnsport.h   27 Jul 2026 14:14:39 -0000      1.24
> +++ services/listen_dnsport.h   20 Sep 2026 09:50:48 -0000
> @@ -347,6 +347,10 @@ struct tcp_req_info {
>         int num_done_req;
>         /** list of pending writable result packets, malloced one at a
> time */
>         struct tcp_req_done_item* done_req_list;
> +       /** the read again timer, when the number of pipelined TCP queries
> +        * is large, it waits, zero time, for a new event loop to service
> +        * the remainder of the TCP traffic on the fd. */
> +       struct comm_timer* read_again_timer;
>  };
>
>  /**
> @@ -377,10 +381,12 @@ struct tcp_req_done_item {
>   * Create tcp request info structure that keeps track of open
>   * requests on the TCP channel that are resolved at the same time,
>   * and the pending results that have to get written back to that client.
> + * @param base: comm base for read again timer.
>   * @param spoolbuf: shared buffer
>   * @return new structure or NULL on alloc failure.
>   */
> -struct tcp_req_info* tcp_req_info_create(struct sldns_buffer* spoolbuf);
> +struct tcp_req_info* tcp_req_info_create(struct comm_base* base,
> +       struct sldns_buffer* spoolbuf);
>
>  /**
>   * Delete tcp request structure.  Called by owning commpoint.
> Index: services/localzone.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/localzone.c,v
> diff -u -p -r1.25 localzone.c
> --- services/localzone.c        26 May 2026 11:14:11 -0000      1.25
> +++ services/localzone.c        20 Sep 2026 09:50:48 -0000
> @@ -386,8 +386,6 @@ new_local_rrset(struct regional* region,
>                 log_err("out of memory");
>                 return NULL;
>         }
> -       rrset->next = node->rrsets;
> -       node->rrsets = rrset;
>         rrset->rrset = (struct ub_packed_rrset_key*)
>                 regional_alloc_zero(region, sizeof(*rrset->rrset));
>         if(!rrset->rrset) {
> @@ -408,6 +406,8 @@ new_local_rrset(struct regional* region,
>         rrset->rrset->rk.dname_len = node->namelen;
>         rrset->rrset->rk.type = htons(rrtype);
>         rrset->rrset->rk.rrset_class = htons(rrclass);
> +       rrset->next = node->rrsets;
> +       node->rrsets = rrset;
>         return rrset;
>  }
>
> @@ -431,6 +431,10 @@ rrset_insert_rr(struct regional* region,
>         pd->rr_ttl = regional_alloc(region, sizeof(*pd->rr_ttl)*pd->count);
>         pd->rr_data = regional_alloc(region,
> sizeof(*pd->rr_data)*pd->count);
>         if(!pd->rr_len || !pd->rr_ttl || !pd->rr_data) {
> +               pd->count--;
> +               pd->rr_len = oldlen;
> +               pd->rr_ttl = oldttl;
> +               pd->rr_data = olddata;
>                 log_err("out of memory");
>                 return 0;
>         }
> @@ -446,6 +450,10 @@ rrset_insert_rr(struct regional* region,
>         pd->rr_ttl[0] = ttl;
>         pd->rr_data[0] = regional_alloc_init(region, rdata, rdata_len);
>         if(!pd->rr_data[0]) {
> +               pd->count--;
> +               pd->rr_len = oldlen;
> +               pd->rr_ttl = oldttl;
> +               pd->rr_data = olddata;
>                 log_err("out of memory");
>                 return 0;
>         }
> @@ -671,7 +679,9 @@ lz_enter_rr_str(struct local_zones* zone
>         z = local_zones_lookup(zones, rr_name, len, labs, rr_class,
> rr_type, 1);
>         if(!z) {
>                 lock_rw_unlock(&zones->lock);
> -               fatal_exit("internal error: no zone for rr %s", rr);
> +               log_err("internal error: no zone for rr %s", rr);
> +               free(rr_name);
> +               return 0;
>         }
>         lock_rw_wrlock(&z->lock);
>         lock_rw_unlock(&zones->lock);
> @@ -1006,23 +1016,23 @@ static struct local_zone* find_closest_p
>         struct local_zone* prev)
>  {
>         struct local_zone* p;
> -        int m;
> +       int m;
>         if(!prev || prev->dclass != curr->dclass) return NULL;
>         (void)dname_lab_cmp(prev->name, prev->namelabs, curr->name,
>                 curr->namelabs, &m); /* we know prev is smaller */
> -                /* sort order like: . com. bla.com. zwb.com. net. */
> -                /* find the previous, or parent-parent-parent */
> +       /* sort order like: . com. bla.com. zwb.com. net. */
> +       /* find the previous, or parent-parent-parent */
>         for(p = prev; p; p = p->parent) {
> -                        /* looking for name with few labels, a parent */
> -                        if(p->namelabs <= m) {
> -                                /* ==: since prev matched m, this is
> closest*/
> -                                /* <: prev matches more, but is not a
> parent,
> -                                 * this one is a (grand)parent */
> +               /* looking for name with few labels, a parent */
> +               if(p->namelabs <= m) {
> +                       /* ==: since prev matched m, this is closest*/
> +                       /* <: prev matches more, but is not a parent,
> +                           * this one is a (grand)parent */
>                         return p;
>                 }
>         }
>         return NULL;
> -                        }
> +}
>
>  /** setup parent pointers, so that a lookup can be done for closest match
> */
>  void
> @@ -1037,7 +1047,7 @@ lz_init_parents(struct local_zones* zone
>                 if(node->override_tree)
>                         addr_tree_init_parents(node->override_tree);
>                 lock_rw_unlock(&node->lock);
> -        }
> +       }
>         lock_rw_unlock(&zones->lock);
>  }
>
> @@ -1500,8 +1510,10 @@ find_tag_datas(struct query_info* qinfo,
>                         return 0; /* out of memory */
>                 qinfo->local_alias->rrset =
>                         regional_alloc_init(temp, r, sizeof(*r));
> -               if(!qinfo->local_alias->rrset)
> +               if(!qinfo->local_alias->rrset) {
> +                       qinfo->local_alias = NULL;
>                         return 0; /* out of memory */
> +               }
>         }
>         return result;
>  }
> @@ -1567,13 +1579,17 @@ local_data_answer(struct local_zone* z,
>                         return 0; /* out of memory */
>                 qinfo->local_alias->rrset = regional_alloc_init(
>                         temp, lr->rrset, sizeof(*lr->rrset));
> -               if(!qinfo->local_alias->rrset)
> +               if(!qinfo->local_alias->rrset) {
> +                       qinfo->local_alias = NULL;
>                         return 0; /* out of memory */
> +               }
>                 qinfo->local_alias->rrset->rk.dname = qinfo->qname;
>                 qinfo->local_alias->rrset->rk.dname_len = qinfo->qname_len;
>                 get_cname_target(lr->rrset, &ctarget, &ctargetlen);
> -               if(!ctargetlen)
> +               if(!ctargetlen) {
> +                       qinfo->local_alias = NULL;
>                         return 0; /* invalid cname */
> +               }
>                 if(dname_is_wild(ctarget)) {
>                         /* synthesize cname target */
>                         struct packed_rrset_data* d, *lr_d;
> @@ -1602,8 +1618,10 @@ local_data_answer(struct local_zone* z,
>                                 sizeof(struct packed_rrset_data) +
> sizeof(size_t) +
>                                 sizeof(uint8_t*) + sizeof(time_t) +
> sizeof(uint16_t)
>                                 + newtargetlen);
> -                       if(!d)
> +                       if(!d) {
> +                               qinfo->local_alias = NULL;
>                                 return 0; /* out of memory */
> +                       }
>                         lr_d = (struct
> packed_rrset_data*)lr->rrset->entry.data;
>                         qinfo->local_alias->rrset->entry.data = d;
>                         d->ttl = lr_d->rr_ttl[0]; /* RFC6672-like behavior:
> @@ -1650,7 +1668,7 @@ local_zone_does_not_cover(struct local_z
>         struct local_data key;
>         struct local_data* ld = NULL;
>         struct local_rrset* lr = NULL;
> -       if(z->type == local_zone_always_transparent || z->type ==
> local_zone_block_a)
> +       if(z->type == local_zone_always_transparent || z->type ==
> local_zone_block_a || z->type == local_zone_block_aaaa)
>                 return 1;
>         if(z->type != local_zone_transparent
>                 && z->type != local_zone_typetransparent
> @@ -1661,7 +1679,9 @@ local_zone_does_not_cover(struct local_z
>         key.namelen = qinfo->qname_len;
>         key.namelabs = labs;
>         ld = (struct local_data*)rbtree_search(&z->data, &key.node);
> -       if(z->type == local_zone_transparent || z->type ==
> local_zone_inform)
> +       if(z->type == local_zone_transparent || z->type ==
> local_zone_inform
> +               || z->type == local_zone_block_a_wdata
> +               || z->type == local_zone_block_aaaa_wdata)
>                 return (ld == NULL);
>         if(ld)
>                 lr = local_data_find_type(ld, qinfo->qtype, 1);
> @@ -1727,7 +1747,8 @@ local_zones_zone_answer(struct local_zon
>                 || lz_type == local_zone_always_transparent) {
>                 /* no NODATA or NXDOMAINS for this zone type */
>                 return 0;
> -       } else if(lz_type == local_zone_block_a) {
> +       } else if(lz_type == local_zone_block_a ||
> +               lz_type == local_zone_block_a_wdata) {
>                 /* Return NODATA for all A queries */
>                 if(qinfo->qtype == LDNS_RR_TYPE_A) {
>                         local_error_encode(qinfo, env, edns, repinfo, buf,
> temp,
> @@ -1737,6 +1758,17 @@ local_zones_zone_answer(struct local_zon
>                 }
>
>                 return 0;
> +       } else if(lz_type == local_zone_block_aaaa ||
> +               lz_type == local_zone_block_aaaa_wdata) {
> +               /* Return NODATA for all AAAA queries */
> +               if(qinfo->qtype == LDNS_RR_TYPE_AAAA) {
> +                       local_error_encode(qinfo, env, edns, repinfo, buf,
> temp,
> +                               LDNS_RCODE_NOERROR,
> (LDNS_RCODE_NOERROR|BIT_AA),
> +                               LDNS_EDE_NONE, NULL);
> +                               return 1;
> +               }
> +
> +               return 0;
>         } else if(lz_type == local_zone_always_null) {
>                 /* 0.0.0.0 or ::0 or noerror/nodata for this zone type,
>                  * used for blocklists. */
> @@ -1904,7 +1936,10 @@ local_zones_answer(struct local_zones* z
>                         lzt == local_zone_typetransparent ||
>                         lzt == local_zone_inform ||
>                         lzt == local_zone_always_transparent ||
> -                       lzt == local_zone_block_a) &&
> +                       lzt == local_zone_block_a ||
> +                       lzt == local_zone_block_aaaa ||
> +                       lzt == local_zone_block_a_wdata ||
> +                       lzt == local_zone_block_aaaa_wdata) &&
>                         local_zone_does_not_cover(z, qinfo, labs)) {
>                         lock_rw_unlock(&z->lock);
>                         z = NULL;
> @@ -1953,6 +1988,7 @@ local_zones_answer(struct local_zones* z
>         if(lzt != local_zone_always_refuse
>                 && lzt != local_zone_always_transparent
>                 && lzt != local_zone_block_a
> +               && lzt != local_zone_block_aaaa
>                 && lzt != local_zone_always_nxdomain
>                 && lzt != local_zone_always_nodata
>                 && lzt != local_zone_always_deny
> @@ -1984,6 +2020,9 @@ const char* local_zone_type2str(enum loc
>                 case local_zone_inform_redirect: return "inform_redirect";
>                 case local_zone_always_transparent: return
> "always_transparent";
>                 case local_zone_block_a: return "block_a";
> +               case local_zone_block_aaaa: return "block_aaaa";
> +               case local_zone_block_a_wdata: return "block_a_wdata";
> +               case local_zone_block_aaaa_wdata: return
> "block_aaaa_wdata";
>                 case local_zone_always_refuse: return "always_refuse";
>                 case local_zone_always_nxdomain: return "always_nxdomain";
>                 case local_zone_always_nodata: return "always_nodata";
> @@ -2020,6 +2059,12 @@ int local_zone_str2type(const char* type
>                 *t = local_zone_always_transparent;
>         else if(strcmp(type, "block_a") == 0)
>                 *t = local_zone_block_a;
> +       else if(strcmp(type, "block_aaaa") == 0)
> +               *t = local_zone_block_aaaa;
> +       else if(strcmp(type, "block_a_wdata") == 0)
> +               *t = local_zone_block_a_wdata;
> +       else if(strcmp(type, "block_aaaa_wdata") == 0)
> +               *t = local_zone_block_aaaa_wdata;
>         else if(strcmp(type, "always_refuse") == 0)
>                 *t = local_zone_always_refuse;
>         else if(strcmp(type, "always_nxdomain") == 0)
> Index: services/localzone.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/localzone.h,v
> diff -u -p -r1.16 localzone.h
> --- services/localzone.h        27 Jul 2026 14:14:39 -0000      1.16
> +++ services/localzone.h        20 Sep 2026 09:50:48 -0000
> @@ -93,6 +93,12 @@ enum localzone_type {
>         local_zone_always_transparent,
>         /** resolve normally, even when there is local data but return
> NODATA for A queries */
>         local_zone_block_a,
> +       /** resolve normally, even when there is local data, but return
> NODATA for AAAA queries */
> +       local_zone_block_aaaa,
> +       /** resolve normally, use local data, else return NODATA for A
> queries */
> +       local_zone_block_a_wdata,
> +       /** resolve normally, use local data, else return NODATA for AAAA
> queries */
> +       local_zone_block_aaaa_wdata,
>         /** answer with error, even when there is local data */
>         local_zone_always_refuse,
>         /** answer with nxdomain, even when there is local data */
> Index: services/mesh.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/mesh.c,v
> diff -u -p -r1.35 mesh.c
> --- services/mesh.c     27 Jul 2026 14:14:39 -0000      1.35
> +++ services/mesh.c     20 Sep 2026 09:50:48 -0000
> @@ -373,7 +373,7 @@ mesh_serve_expired_lookup(struct module_
>                         "validation");
>                 goto bail_out; /* need to validate cache entry first */
>         } else if(msg->rep->security == sec_status_secure &&
> -               !reply_all_rrsets_secure(msg->rep) && must_validate) {
> +               !reply_an_ns_rrsets_secure(msg->rep) && must_validate) {
>                         verbose(VERB_ALGO, "Serve expired: secure entry"
>                                 " changed status");
>                         goto bail_out; /* rrset changed, re-verify */
> @@ -424,6 +424,44 @@ mesh_serve_expired_init(struct mesh_stat
>         return 1;
>  }
>
> +/** remove a reply without accounting, rollback the add reply. */
> +static void
> +mesh_remove_reply_without_accounting(struct mesh_state* s,
> +       struct mesh_reply* todel)
> +{
> +       struct mesh_reply* r, *prev = NULL;
> +       for(r = s->reply_list; r; r = r->next) {
> +               if(r == todel) {
> +                       if(prev)
> +                               prev->next = r->next;
> +                       else    s->reply_list = r->next;
> +                       r->next = NULL;
> +                       /* todel is allocated in region */
> +                       return;
> +               }
> +               prev = r;
> +       }
> +}
> +
> +/** remove a callback without accounting, rollback the add reply. */
> +static void
> +mesh_remove_callback_without_accounting(struct mesh_state* s,
> +       struct mesh_cb* todel)
> +{
> +       struct mesh_cb* r, *prev = NULL;
> +       for(r = s->cb_list; r; r = r->next) {
> +               if(r == todel) {
> +                       if(prev)
> +                               prev->next = r->next;
> +                       else    s->cb_list = r->next;
> +                       r->next = NULL;
> +                       /* todel is allocated in region */
> +                       return;
> +               }
> +               prev = r;
> +       }
> +}
> +
>  void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo,
>         struct respip_client_info* cinfo, uint16_t qflags,
>         struct edns_data* edns, struct comm_reply* rep, uint16_t qid,
> @@ -433,7 +471,8 @@ void mesh_new_client(struct mesh_area* m
>         int unique = unique_mesh_state(edns->opt_list_in, mesh->env);
>         int was_detached = 0;
>         int was_noreply = 0;
> -       int added = 0;
> +       int added = 0, added_reply_without_accounting = 0, added_tcp = 0;
> +       struct mesh_reply* repadded = NULL;
>         int timeout = mesh->env->cfg->serve_expired?
>                 mesh->env->cfg->serve_expired_client_timeout:0;
>         struct sldns_buffer* r_buffer = rep->c->buffer;
> @@ -544,16 +583,18 @@ void mesh_new_client(struct mesh_area* m
>                 }
>         }
>         /* add reply to s */
> -       if(!mesh_state_add_reply(s, edns, rep, qid, qflags, qinfo)) {
> +       if(!mesh_state_add_reply(s, edns, rep, qid, qflags, qinfo,
> &repadded)) {
>                 log_err("mesh_new_client: out of memory; SERVFAIL");
>                 goto servfail_mem;
>         }
> +       added_reply_without_accounting = 1;
>         if(rep->c->tcp_req_info) {
>                 if(!tcp_req_info_add_meshstate(rep->c->tcp_req_info, mesh,
> s)) {
>                         log_err("mesh_new_client: out of memory add
> tcpreqinfo");
>                         goto servfail_mem;
>                 }
>         }
> +       added_tcp = 1;
>         if(rep->c->use_h2) {
>                 http2_stream_add_meshstate(rep->c->h2_stream, mesh, s);
>         } else if(rep->c->type == comm_doq && rep->doq_stream) {
> @@ -575,6 +616,8 @@ void mesh_new_client(struct mesh_area* m
>                 }
>         }
>  #endif
> +       /* Since the acccounting now happens,
> +        * added_reply_without_accounting = 0;   but that is not used. */
>         infra_wait_limit_inc(mesh->env->infra_cache, rep, *mesh->env->now,
>                 mesh->env->cfg);
>         /* update statistics */
> @@ -614,6 +657,11 @@ servfail_mem:
>         else if(rep->c->type == comm_doq && rep->doq_stream)
>                 doq_stream_remove_mesh_state(rep->doq_stream);
>         comm_point_send_reply(rep);
> +       if(added_reply_without_accounting) {
> +               mesh_remove_reply_without_accounting(s, repadded);
> +               if(added_tcp && rep->c->tcp_req_info)
> +
>  tcp_req_info_remove_mesh_state(rep->c->tcp_req_info, s);
> +       }
>         if(added)
>                 mesh_state_delete(&s->s);
>         return;
> @@ -622,7 +670,8 @@ servfail_mem:
>  int
>  mesh_new_callback(struct mesh_area* mesh, struct query_info* qinfo,
>         uint16_t qflags, struct edns_data* edns, sldns_buffer* buf,
> -       uint16_t qid, mesh_cb_func_type cb, void* cb_arg, int rpz_passthru)
> +       uint16_t qid, mesh_cb_func_type cb, void* cb_arg, int rpz_passthru,
> +       void** unique_info)
>  {
>         struct mesh_state* s = NULL;
>         int unique = unique_mesh_state(edns->opt_list_in, mesh->env);
> @@ -631,6 +680,7 @@ mesh_new_callback(struct mesh_area* mesh
>         int was_detached = 0;
>         int was_noreply = 0;
>         int added = 0;
> +       struct mesh_cb* add_cb = NULL;
>         uint16_t mesh_flags = qflags&(BIT_RD|BIT_CD);
>         if(!unique)
>                 s = mesh_area_find(mesh, NULL, qinfo, mesh_flags, 0, 0);
> @@ -676,13 +726,14 @@ mesh_new_callback(struct mesh_area* mesh
>                 }
>         }
>         /* add reply to s */
> -       if(!mesh_state_add_cb(s, edns, buf, cb, cb_arg, qid, qflags)) {
> +       if(!mesh_state_add_cb(s, edns, buf, cb, cb_arg, qid, qflags,
> &add_cb)) {
>                 if(added)
>                         mesh_state_delete(&s->s);
>                 return 0;
>         }
>         /* add serve expired timer if not already there */
>         if(timeout && !mesh_serve_expired_init(s, timeout)) {
> +               mesh_remove_callback_without_accounting(s, add_cb);
>                 if(added)
>                         mesh_state_delete(&s->s);
>                 return 0;
> @@ -693,6 +744,7 @@ mesh_new_callback(struct mesh_area* mesh
>                 (mesh->env->cachedb_enabled &&
>                  mesh->env->cfg->cachedb_check_when_serve_expired)) {
>                 if(!mesh_serve_expired_init(s, -1)) {
> +                       mesh_remove_callback_without_accounting(s, add_cb);
>                         if(added)
>                                 mesh_state_delete(&s->s);
>                         return 0;
> @@ -708,6 +760,8 @@ mesh_new_callback(struct mesh_area* mesh
>                 mesh->num_reply_states ++;
>         }
>         mesh->num_reply_addrs++;
> +       if(unique_info)
> +               *unique_info = s->unique;
>         if(added)
>                 mesh_run(mesh, s, module_event_new, NULL);
>         return 1;
> @@ -911,32 +965,9 @@ void mesh_report_reply(struct mesh_area*
>         mesh_run(mesh, e->qstate->mesh_info, event, e);
>  }
>
> -/** copy strlist to region */
> -static struct config_strlist*
> -cfg_region_strlist_copy(struct regional* region, struct config_strlist*
> list)
> -{
> -       struct config_strlist* result = NULL, *last = NULL, *s = list;
> -       while(s) {
> -               struct config_strlist* n = regional_alloc_zero(region,
> -                       sizeof(*n));
> -               if(!n)
> -                       return NULL;
> -               n->str = regional_strdup(region, s->str);
> -               if(!n->str)
> -                       return NULL;
> -               if(last)
> -                       last->next = n;
> -               else    result = n;
> -               last = n;
> -               s = s->next;
> -       }
> -       return result;
> -}
> -
>  struct respip_client_info*
>  mesh_copy_client_info(struct regional* region, struct respip_client_info*
> cinfo)
>  {
> -       size_t i;
>         struct respip_client_info* client_info;
>         client_info = regional_alloc_init(region, cinfo, sizeof(*cinfo));
>         if(!client_info)
> @@ -955,20 +986,13 @@ mesh_copy_client_info(struct regional* r
>                 if(!client_info->tag_actions)
>                         return NULL;
>         }
> -       if(cinfo->tag_datas) {
> -               client_info->tag_datas = regional_alloc_zero(region,
> -                       sizeof(struct
> config_strlist*)*cinfo->tag_datas_size);
> -               if(!client_info->tag_datas)
> -                       return NULL;
> -               for(i=0; i<cinfo->tag_datas_size; i++) {
> -                       if(cinfo->tag_datas[i]) {
> -                               client_info->tag_datas[i] =
> cfg_region_strlist_copy(
> -                                       region, cinfo->tag_datas[i]);
> -                               if(!client_info->tag_datas[i])
> -                                       return NULL;
> -                       }
> -               }
> -       }
> +       /* tag_datas is owned by the matched acl_addr in config_file; its
> +        * lifetime is until config reload, which tears down all mesh
> states
> +        * first. Keep the original pointer so client_info_compare()
> +        * can recognise two states from the same ACL entry. */
> +       /* fast reload insists on dropping the queries when
> interface-tag-data
> +        * or access-control-tag-data are changed. */
> +       /* client_info->tag_datas already copied by regional_alloc_init
> above */
>         if(cinfo->view) {
>                 /* Do not copy the view pointer but store a name instead.
>                  * The name is looked up later when done, this means that
> @@ -1073,6 +1097,18 @@ mesh_state_make_unique(struct mesh_state
>         mstate->unique = mstate;
>  }
>
> +/** pop a reply from the reply list, if there are any. */
> +static struct mesh_reply*
> +mesh_reply_list_pop_first(struct mesh_state* mstate)
> +{
> +       if(mstate->reply_list) {
> +               struct mesh_reply* r = mstate->reply_list;
> +               mstate->reply_list = r->next;
> +               return r;
> +       }
> +       return NULL;
> +}
> +
>  void
>  mesh_state_cleanup(struct mesh_state* mstate)
>  {
> @@ -1088,23 +1124,30 @@ mesh_state_cleanup(struct mesh_state* ms
>         }
>         /* drop unsent replies */
>         if(!mstate->replies_sent) {
> -               struct mesh_reply* rep = mstate->reply_list;
> +               struct mesh_reply* rep;
>                 struct mesh_cb* cb;
> -               /* One http2 stream could bring down its comm_point along
> with
> -                * the other streams which could share the same query. Do
> all
> -                * the http2 stream bookkeeping upfront. */
> -               for(; rep; rep=rep->next) {
> -                       if(rep->query_reply.c->use_h2)
> -
>  http2_stream_remove_mesh_state(rep->h2_stream);
> -               }
> -               rep = mstate->reply_list;
> -               /* in tcp_req_info, the mstates linked are removed, but
> -                * the reply_list is now NULL, so the
> remove-from-empty-list
> -                * takes no time and also it does not do the mesh
> accounting */
> -               mstate->reply_list = NULL;
> -               for(; rep; rep=rep->next) {
> +               /* Pop items from the list, that means there is no
> iterator.
> +                * And then items can be removed from the reply list, from
> +                * like comm_point_drop_reply and comm_point_close calls.
> +                * As the tcp_req_info and http2 code drops the entire
> +                * connection. That could delete mesh_reply items previous
> and
> +                * after the current state. The previous items are already
> +                * popped. And the next items can be altered, like to when
> a
> +                * connection has more replies on the reply list.
> +                * The current item is also popped so the code needs to
> +                * remove its references. */
> +               while((rep = mesh_reply_list_pop_first(mstate)) != NULL) {
>                         infra_wait_limit_dec(mesh->env->infra_cache,
>                                 &rep->query_reply, mesh->env->cfg);
> +                       if(rep->query_reply.c->tcp_req_info)
> +                               tcp_req_info_remove_mesh_state(
> +                                       rep->query_reply.c->tcp_req_info,
> +                                       mstate);
> +                       else if(rep->query_reply.c->use_h2)
> +
>  http2_stream_remove_mesh_state(rep->h2_stream);
> +                       else if(rep->query_reply.doq_stream)
> +                               doq_stream_remove_mesh_state(
> +                                       rep->query_reply.doq_stream);
>                         comm_point_drop_reply(&rep->query_reply);
>                         log_assert(mesh->num_reply_addrs > 0);
>                         mesh->num_reply_addrs--;
> @@ -1241,6 +1284,9 @@ int mesh_add_sub(struct module_qstate* q
>                         log_err("mesh_attach_sub: out of memory");
>                         return 0;
>                 }
> +               /* inherit RPZ passthru from the parent so respip on the
> sub
> +                * sees the same client-IP/qname PASSTHRU decision */
> +               (*sub)->s.rpz_passthru = qstate->rpz_passthru;
>  #ifdef UNBOUND_DEBUG
>                 n =
>  #else
> @@ -1465,12 +1511,6 @@ mesh_send_reply(struct mesh_state* m, in
>         struct timeval end_time;
>         struct timeval duration;
>         int secure;
> -       /* briefly set the replylist to null in case the
> -        * meshsendreply calls tcpreqinfo sendreply that
> -        * comm_point_drops because of size, and then the
> -        * null stops the mesh state remove and thus
> -        * reply_list modification and accounting */
> -       struct mesh_reply* rlist = m->reply_list;
>
>         /* rpz: apply actions */
>         rcode = mesh_is_udp(r) && mesh_is_rpz_respip_tcponly_action(m)
> @@ -1527,9 +1567,7 @@ mesh_send_reply(struct mesh_state* m, in
>                 sldns_buffer_write_at(r_buffer, 0, &r->qid,
> sizeof(uint16_t));
>                 sldns_buffer_write_at(r_buffer, 12, r->qname,
>                         m->s.qinfo.qname_len);
> -               m->reply_list = NULL;
>                 comm_point_send_reply(&r->query_reply);
> -               m->reply_list = rlist;
>         } else if(rcode) {
>                 m->s.qinfo.qname = r->qname;
>                 m->s.qinfo.local_alias = r->local_alias;
> @@ -1551,9 +1589,7 @@ mesh_send_reply(struct mesh_state* m, in
>                 }
>                 error_encode(r_buffer, rcode, &m->s.qinfo, r->qid,
>                         r->qflags, &r->edns);
> -               m->reply_list = NULL;
>                 comm_point_send_reply(&r->query_reply);
> -               m->reply_list = rlist;
>         } else {
>                 size_t udp_size = r->edns.udp_size;
>                 r->edns.edns_version = EDNS_ADVERTISED_VERSION;
> @@ -1589,9 +1625,7 @@ mesh_send_reply(struct mesh_state* m, in
>                         error_encode(r_buffer, LDNS_RCODE_SERVFAIL,
>                                 &m->s.qinfo, r->qid, r->qflags, &r->edns);
>                 }
> -               m->reply_list = NULL;
>                 comm_point_send_reply(&r->query_reply);
> -               m->reply_list = rlist;
>         }
>         infra_wait_limit_dec(m->s.env->infra_cache, &r->query_reply,
>                 m->s.env->cfg);
> @@ -1743,6 +1777,7 @@ void mesh_query_done(struct mesh_state*
>         struct reply_info* rep = (mstate->s.return_msg?
>                 mstate->s.return_msg->rep:NULL);
>         struct timeval tv = {0, 0};
> +       struct mesh_area* mesh = mstate->s.env->mesh;
>         int i = 0;
>         /* No need for the serve expired timer anymore; we are going to
> reply. */
>         if(mstate->s.serve_expired_data) {
> @@ -1763,10 +1798,22 @@ void mesh_query_done(struct mesh_state*
>                 }
>         }
>
> -       if(mstate->reply_list && mstate->s.env->cfg->dns_error_reporting)
> +       if(mstate->reply_list && mstate->s.env->cfg->dns_error_reporting
> +               && (!rep || rep->security != sec_status_secure))
>                 dns_error_reporting(&mstate->s, rep);
>
> -       for(r = mstate->reply_list; r; r = r->next) {
> +       while((r = mesh_reply_list_pop_first(mstate)) != NULL) {
> +
> +               /* it was not detached (because it had a reply list),
> could be now */
> +               if(!mstate->reply_list && !mstate->cb_list
> +                       && mstate->super_set.count == 0) {
> +                       mesh->num_detached_states++;
> +               }
> +               /* if not replies any more in mstate, it is no longer a
> reply_state */
> +               if(!mstate->reply_list && !mstate->cb_list) {
> +                       log_assert(mesh->num_reply_states > 0);
> +                       mesh->num_reply_states--;
> +               }
>                 if(mesh_is_udp(r)) {
>                         /* For UDP queries, the old replies are discarded.
>                          * This stops a large volume of old replies from
> @@ -1781,22 +1828,18 @@ void mesh_query_done(struct mesh_state*
>
> ((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
>                                 mstate->s.env->cfg->discard_timeout) {
>                                 /* Drop the reply, it is too old */
> -                               /* briefly set the reply_list to NULL, so
> that the
> -                                * tcp req info cleanup routine that calls
> the mesh
> -                                * to deregister the meshstate for it is
> not done
> -                                * because the list is NULL and also
> accounting is not
> -                                * done there, but instead we do that
> here. */
> -                               struct mesh_reply* reply_list =
> mstate->reply_list;
>                                 verbose(VERB_ALGO, "drop reply, it is
> older than discard-timeout");
>
> infra_wait_limit_dec(mstate->s.env->infra_cache,
>                                         &r->query_reply,
> mstate->s.env->cfg);
> -                               mstate->reply_list = NULL;
> -                               if(r->query_reply.c->use_h2)
> +                               if(r->query_reply.c->tcp_req_info)
> +                                       tcp_req_info_remove_mesh_state(
> +
>  r->query_reply.c->tcp_req_info,
> +                                               mstate);
> +                               else if(r->query_reply.c->use_h2)
>
> http2_stream_remove_mesh_state(r->h2_stream);
>                                 else if(r->query_reply.doq_stream)
>
> doq_stream_remove_mesh_state(r->query_reply.doq_stream);
>                                 comm_point_drop_reply(&r->query_reply);
> -                               mstate->reply_list = reply_list;
>
> log_assert(mstate->s.env->mesh->num_reply_addrs > 0);
>                                 mstate->s.env->mesh->num_reply_addrs--;
>
> mstate->s.env->mesh->num_queries_discard_timeout++;
> @@ -1821,22 +1864,17 @@ void mesh_query_done(struct mesh_state*
>                 /* if this query is determined to be dropped during the
>                  * mesh processing, this is the point to take that action.
> */
>                 if(mstate->s.is_drop) {
> -                       /* briefly set the reply_list to NULL, so that the
> -                        * tcp req info cleanup routine that calls the mesh
> -                        * to deregister the meshstate for it is not done
> -                        * because the list is NULL and also accounting is
> not
> -                        * done there, but instead we do that here. */
> -                       struct mesh_reply* reply_list = mstate->reply_list;
>                         infra_wait_limit_dec(mstate->s.env->infra_cache,
>                                 &r->query_reply, mstate->s.env->cfg);
> -                       mstate->reply_list = NULL;
> -                       if(r->query_reply.c->use_h2) {
> +                       if(r->query_reply.c->tcp_req_info) {
> +                               tcp_req_info_remove_mesh_state(
> +                                       r->query_reply.c->tcp_req_info,
> mstate);
> +                       } else if(r->query_reply.c->use_h2) {
>
> http2_stream_remove_mesh_state(r->h2_stream);
>                         } else if(r->query_reply.doq_stream) {
>
> doq_stream_remove_mesh_state(r->query_reply.doq_stream);
>                         }
>                         comm_point_drop_reply(&r->query_reply);
> -                       mstate->reply_list = reply_list;
>                         log_assert(mstate->s.env->mesh->num_reply_addrs >
> 0);
>                         mstate->s.env->mesh->num_reply_addrs--;
>                 } else {
> @@ -1877,18 +1915,6 @@ void mesh_query_done(struct mesh_state*
>                 }
>         }
>
> -       /* Mesh area accounting */
> -       if(mstate->reply_list) {
> -               mstate->reply_list = NULL;
> -               if(!mstate->reply_list && !mstate->cb_list) {
> -                       /* was a reply state, not anymore */
> -                       log_assert(mstate->s.env->mesh->num_reply_states >
> 0);
> -                       mstate->s.env->mesh->num_reply_states--;
> -               }
> -               if(!mstate->reply_list && !mstate->cb_list &&
> -                       mstate->super_set.count == 0)
> -                       mstate->s.env->mesh->num_detached_states++;
> -       }
>         mstate->replies_sent = 1;
>
>         while((c = mstate->cb_list) != NULL) {
> @@ -1946,6 +1972,25 @@ struct mesh_state* mesh_area_find(struct
>         return result;
>  }
>
> +struct mesh_state* mesh_area_find_unique(struct mesh_area* mesh,
> +       struct respip_client_info* cinfo, struct query_info* qinfo,
> +       uint16_t qflags, int prime, int valrec, void* unique_info)
> +{
> +       struct mesh_state key;
> +       struct mesh_state* result;
> +
> +       key.node.key = &key;
> +       key.s.is_priming = prime;
> +       key.s.is_valrec = valrec;
> +       key.s.qinfo = *qinfo;
> +       key.s.query_flags = qflags;
> +       key.unique = (struct mesh_state*)unique_info;
> +       key.s.client_info = cinfo;
> +
> +       result = (struct mesh_state*)rbtree_search(&mesh->all, &key);
> +       return result;
> +}
> +
>  /** remove mesh state callback */
>  int mesh_state_del_cb(struct mesh_state* s, mesh_cb_func_type cb, void*
> cb_arg)
>  {
> @@ -1967,7 +2012,7 @@ int mesh_state_del_cb(struct mesh_state*
>
>  int mesh_state_add_cb(struct mesh_state* s, struct edns_data* edns,
>          sldns_buffer* buf, mesh_cb_func_type cb, void* cb_arg,
> -       uint16_t qid, uint16_t qflags)
> +       uint16_t qid, uint16_t qflags, struct mesh_cb** result)
>  {
>         struct mesh_cb* r = regional_alloc(s->s.region,
>                 sizeof(struct mesh_cb));
> @@ -1991,13 +2036,14 @@ int mesh_state_add_cb(struct mesh_state*
>         r->qflags = qflags;
>         r->next = s->cb_list;
>         s->cb_list = r;
> +       *result = r;
>         return 1;
>
>  }
>
>  int mesh_state_add_reply(struct mesh_state* s, struct edns_data* edns,
>          struct comm_reply* rep, uint16_t qid, uint16_t qflags,
> -        const struct query_info* qinfo)
> +        const struct query_info* qinfo, struct mesh_reply** result)
>  {
>         struct mesh_reply* r = regional_alloc(s->s.region,
>                 sizeof(struct mesh_reply));
> @@ -2078,6 +2124,7 @@ int mesh_state_add_reply(struct mesh_sta
>                 r->local_alias = NULL;
>
>         s->reply_list = r;
> +       *result = r;
>         return 1;
>  }
>
> @@ -2235,8 +2282,29 @@ void mesh_run(struct mesh_area* mesh, st
>         enum module_ev ev, struct outbound_entry* e)
>  {
>         enum module_ext_state s;
> +       int numrun = 0;
>         verbose(VERB_ALGO, "mesh_run: start");
>         while(mstate) {
> +               if(numrun++ > MESH_MAX_RUN_ITER) {
> +                       /* These modules are too much to activate, stop
> them.*/
> +                       log_err("Too many module run iterations,
> deleting");
> +                       while(mstate) {
> +                               /* notify supers */
> +                               if(mstate->super_set.count > 0) {
> +                                       verbose(VERB_ALGO, "notify supers
> of failure");
> +                                       mstate->s.return_msg = NULL;
> +                                       mstate->s.return_rcode =
> LDNS_RCODE_SERVFAIL;
> +                                       mesh_walk_supers(mesh, mstate);
> +                               }
> +                               mesh_state_delete(&mstate->s);
> +                               if(mesh->run.count > 0) {
> +                                       /* pop random element off the
> runnable tree */
> +                                       mstate = (struct
> mesh_state*)mesh->run.root->key;
> +                                       (void)rbtree_delete(&mesh->run,
> mstate);
> +                               } else mstate = NULL;
> +                       }
> +                       break;
> +               }
>                 /* run the module */
>                 fptr_ok(fptr_whitelist_mod_operate(
>                         mesh->mods.mod[mstate->s.curmod]->operate));
> @@ -2388,7 +2456,8 @@ void mesh_list_remove(struct mesh_state*
>  }
>
>  void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m,
> -       struct comm_point* cp, struct doq_stream* doq_stream)
> +       struct comm_point* cp, struct http2_stream* h2_stream,
> +       struct doq_stream* doq_stream)
>  {
>         struct mesh_reply* n, *prev = NULL;
>         n = m->reply_list;
> @@ -2397,6 +2466,7 @@ void mesh_state_remove_reply(struct mesh
>         if(!n) return; /* nothing to remove, also no accounting needed */
>         while(n) {
>                 if(n->query_reply.c == cp
> +                       && (!h2_stream || n->h2_stream == h2_stream)
>                         && (!doq_stream || n->query_reply.doq_stream ==
> doq_stream)) {
>                         /* unlink it */
>                         if(prev) prev->next = n->next;
> @@ -2434,7 +2504,6 @@ void mesh_state_remove_reply(struct mesh
>         }
>  }
>
> -
>  static int
>  apply_respip_action(struct module_qstate* qstate,
>         const struct query_info* qinfo, struct respip_client_info* cinfo,
> @@ -2567,7 +2636,18 @@ mesh_serve_expired_callback(void* arg)
>         if(verbosity >= VERB_ALGO)
>                 log_dns_msg("Serve expired lookup", &qstate->qinfo,
> msg->rep);
>
> -       for(r = mstate->reply_list; r; r = r->next) {
> +       while((r = mesh_reply_list_pop_first(mstate)) != NULL) {
> +
> +               /* it was not detached (because it had a reply list),
> could be now */
> +               if(!mstate->reply_list && !mstate->cb_list
> +                       && mstate->super_set.count == 0) {
> +                       mesh->num_detached_states++;
> +               }
> +               /* if not replies any more in mstate, it is no longer a
> reply_state */
> +               if(!mstate->reply_list && !mstate->cb_list) {
> +                       log_assert(mesh->num_reply_states > 0);
> +                       mesh->num_reply_states--;
> +               }
>                 if(mesh_is_udp(r)) {
>                     struct timeval old;
>                     timeval_subtract(&old, mstate->s.env->now_tv,
> &r->start_time);
> @@ -2575,22 +2655,17 @@ mesh_serve_expired_callback(void* arg)
>                         ((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 >
>                         mstate->s.env->cfg->discard_timeout) {
>                         /* Drop the reply, it is too old */
> -                       /* briefly set the reply_list to NULL, so that the
> -                        * tcp req info cleanup routine that calls the mesh
> -                        * to deregister the meshstate for it is not done
> -                        * because the list is NULL and also accounting is
> not
> -                        * done there, but instead we do that here. */
> -                       struct mesh_reply* reply_list = mstate->reply_list;
>                         verbose(VERB_ALGO, "drop reply, it is older than
> discard-timeout");
>                         infra_wait_limit_dec(mstate->s.env->infra_cache,
>                                 &r->query_reply, mstate->s.env->cfg);
> -                       mstate->reply_list = NULL;
> -                       if(r->query_reply.c->use_h2)
> +                       if(r->query_reply.c->tcp_req_info)
> +                               tcp_req_info_remove_mesh_state(
> +                                       r->query_reply.c->tcp_req_info,
> mstate);
> +                       else if(r->query_reply.c->use_h2)
>
> http2_stream_remove_mesh_state(r->h2_stream);
>                         else if(r->query_reply.doq_stream)
>
> doq_stream_remove_mesh_state(r->query_reply.doq_stream);
>                         comm_point_drop_reply(&r->query_reply);
> -                       mstate->reply_list = reply_list;
>                         log_assert(mstate->s.env->mesh->num_reply_addrs >
> 0);
>                         mstate->s.env->mesh->num_reply_addrs--;
>                         mstate->s.env->mesh->num_queries_discard_timeout++;
> @@ -2628,8 +2703,7 @@ mesh_serve_expired_callback(void* arg)
>                 if(r->query_reply.c->tcp_req_info)
>
> tcp_req_info_remove_mesh_state(r->query_reply.c->tcp_req_info, mstate);
>                 /* mesh_send_reply removed mesh state from http2_stream. */
> -               infra_wait_limit_dec(mstate->s.env->infra_cache,
> -                       &r->query_reply, mstate->s.env->cfg);
> +               /* mesh_send_reply decremented wait_limit. */
>                 prev = r;
>                 prev_buffer = r_buffer;
>         }
> @@ -2648,18 +2722,6 @@ mesh_serve_expired_callback(void* arg)
>                 }
>         }
>
> -       /* Mesh area accounting */
> -       if(mstate->reply_list) {
> -               mstate->reply_list = NULL;
> -               if(!mstate->reply_list && !mstate->cb_list) {
> -                       log_assert(mesh->num_reply_states > 0);
> -                       mesh->num_reply_states--;
> -                       if(mstate->super_set.count == 0) {
> -                               mesh->num_detached_states++;
> -                       }
> -               }
> -       }
> -
>         while((c = mstate->cb_list) != NULL) {
>                 /* take this cb off the list; so that the list can be
>                  * changed, eg. by adds from the callback routine */
> @@ -2692,13 +2754,30 @@ int mesh_jostle_exceeded(struct mesh_are
>  }
>
>  void mesh_remove_callback(struct mesh_area* mesh, struct query_info*
> qinfo,
> -       uint16_t qflags, mesh_cb_func_type cb, void* cb_arg)
> +       uint16_t qflags, mesh_cb_func_type cb, void* cb_arg, void*
> unique_info)
>  {
>         struct mesh_state* s = NULL;
>         s = mesh_area_find(mesh, NULL, qinfo, qflags&(BIT_RD|BIT_CD), 0,
> 0);
> -       if(!s) return;
> -       if(!mesh_state_del_cb(s, cb, cb_arg)) return;
> +       if(s && mesh_state_del_cb(s, cb, cb_arg))
> +               goto removed;
> +       if(unique_info) {
> +               s = mesh_area_find_unique(mesh, NULL, qinfo,
> +                       qflags&(BIT_RD|BIT_CD), 0, 0, unique_info);
> +               if(s && mesh_state_del_cb(s, cb, cb_arg))
> +                       goto removed;
> +       }
> +       /* mesh_area_find builds key.unique=NULL and cannot match a state
> +        * created with mesh_state_make_unique (e.g. subnetcache sets
> +        * env->unique_mesh). Fall back to a linear scan; cb+cb_arg is an
> +        * exact key (mesh_state_del_cb compares both).
> +        * This works for both lookups for zonemd and for hostname
> authzone. */
> +       RBTREE_FOR(s, struct mesh_state*, &mesh->all) {
> +               if(s->cb_list && mesh_state_del_cb(s, cb, cb_arg))
> +                       goto removed;
> +       }
> +       return;
>
> +removed:
>         /* It was in the list and removed. */
>         log_assert(mesh->num_reply_addrs > 0);
>         mesh->num_reply_addrs--;
> Index: services/mesh.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/mesh.h,v
> diff -u -p -r1.17 mesh.h
> --- services/mesh.h     27 Jul 2026 14:14:39 -0000      1.17
> +++ services/mesh.h     20 Sep 2026 09:50:48 -0000
> @@ -70,6 +70,13 @@ struct respip_client_info;
>  #define MESH_MAX_ACTIVATION 10000
>
>  /**
> + * Maximum number of mesh state run items. These are different modules
> + * activated during a mesh run. Any more is likely an infinite loop
> + * in the module. It is then terminated, and states are deleted.
> + */
> +#define MESH_MAX_RUN_ITER 10000
> +
> +/**
>   * Max number of references-to-references-to-references.. search size.
>   * Any more is treated like 'too large', and the creation of a new
>   * dependency is failed (so that no loops can be created).
> @@ -342,11 +349,14 @@ void mesh_new_client(struct mesh_area* m
>   * @param cb_arg: callback user arg.
>   * @param rpz_passthru: if true, the rpz passthru was previously found and
>   *     further rpz processing is stopped.
> + * @param unique_info: if nonnull, unique info is passed back to be used
> + *     for the callback remove call. It does not need to be deallocated.
>   * @return 0 on error.
>   */
>  int mesh_new_callback(struct mesh_area* mesh, struct query_info* qinfo,
>         uint16_t qflags, struct edns_data* edns, struct sldns_buffer* buf,
> -       uint16_t qid, mesh_cb_func_type cb, void* cb_arg, int
> rpz_passthru);
> +       uint16_t qid, mesh_cb_func_type cb, void* cb_arg, int rpz_passthru,
> +       void** unique_info);
>
>  /**
>   * New prefetch message. Create new query state if needed.
> @@ -544,6 +554,23 @@ struct mesh_state* mesh_area_find(struct
>         uint16_t qflags, int prime, int valrec);
>
>  /**
> + * Find a unique mesh state in the mesh area. Pass relevant flags.
> + *
> + * @param mesh: the mesh area to look in.
> + * @param cinfo: if non-NULL client specific info that may affect IP-based
> + *     actions that apply to the query result.
> + * @param qinfo: what query
> + * @param qflags: if RD / CD bit is set or not.
> + * @param prime: if it is a priming query.
> + * @param valrec: if it is a validation-recursion query.
> + * @param unique_info: the unique info for the state. NULL can be passed.
> + * @return: mesh state or NULL if not found.
> + */
> +struct mesh_state* mesh_area_find_unique(struct mesh_area* mesh,
> +       struct respip_client_info* cinfo, struct query_info* qinfo,
> +       uint16_t qflags, int prime, int valrec, void* unique_info);
> +
> +/**
>   * Setup attachment super/sub relation between super and sub mesh state.
>   * The relation must not be present when calling the function.
>   * Does not update stat items in mesh_area.
> @@ -562,11 +589,12 @@ int mesh_state_attachment(struct mesh_st
>   * @param qid: ID of reply.
>   * @param qflags: original query flags.
>   * @param qinfo: original query info.
> + * @param result: the allocated reply structure, for rollback.
>   * @return: 0 on alloc error.
>   */
>  int mesh_state_add_reply(struct mesh_state* s, struct edns_data* edns,
>         struct comm_reply* rep, uint16_t qid, uint16_t qflags,
> -       const struct query_info* qinfo);
> +       const struct query_info* qinfo, struct mesh_reply** result);
>
>  /**
>   * Create new callback structure and attach it to a mesh state.
> @@ -578,11 +606,12 @@ int mesh_state_add_reply(struct mesh_sta
>   * @param cb_arg: callback user arg.
>   * @param qid: ID of reply.
>   * @param qflags: original query flags.
> + * @param result: the allocated callback structure, for rollback.
>   * @return: 0 on alloc error.
>   */
>  int mesh_state_add_cb(struct mesh_state* s, struct edns_data* edns,
>          struct sldns_buffer* buf, mesh_cb_func_type cb, void* cb_arg,
> -       uint16_t qid, uint16_t qflags);
> +       uint16_t qid, uint16_t qflags, struct mesh_cb** result);
>
>  /**
>   * Run the mesh. Run all runnable mesh states. Which can create new
> @@ -683,11 +712,14 @@ void mesh_list_remove(struct mesh_state*
>   * @param mesh: to update the counters.
>   * @param m: the mesh state.
>   * @param cp: the comm_point to remove from the list.
> + * @param h2_stream: if not NULL, it specifies the h2_stream to match
> + *     for the delete.
>   * @param doq_stream: if not NULL, it specifies the doq_stream to match
>   *     for the delete.
>   */
>  void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m,
> -       struct comm_point* cp, struct doq_stream* doq_stream);
> +       struct comm_point* cp, struct http2_stream* h2_stream,
> +       struct doq_stream* doq_stream);
>
>  /** Callback for when the serve expired client timer has run out.  Tries
> to
>   * find an expired answer in the cache and reply that to the client.
> @@ -734,9 +766,10 @@ void mesh_respond_serve_expired(struct m
>   * @param qflags: flags from client query.
>   * @param cb: callback function.
>   * @param cb_arg: callback user arg.
> + * @param unique_info: if not NULL, used to find a unique state for
> removal.
>   */
>  void mesh_remove_callback(struct mesh_area* mesh, struct query_info*
> qinfo,
> -       uint16_t qflags, mesh_cb_func_type cb, void* cb_arg);
> +       uint16_t qflags, mesh_cb_func_type cb, void* cb_arg, void*
> unique_info);
>
>  /** Copy the client info to the query region. */
>  struct respip_client_info* mesh_copy_client_info(struct regional* region,
> Index: services/modstack.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/modstack.c,v
> diff -u -p -r1.12 modstack.c
> --- services/modstack.c 26 May 2026 11:14:11 -0000      1.12
> +++ services/modstack.c 20 Sep 2026 09:50:48 -0000
> @@ -232,7 +232,7 @@ module_func_block* module_factory(const
>          return NULL;
>  }
>
> -int
> +int
>  modstack_call_startup(struct module_stack* stack, const char* module_conf,
>         struct module_env* env)
>  {
> @@ -301,7 +301,7 @@ modstack_call_init(struct module_stack*
>         return 1;
>  }
>
> -void
> +void
>  modstack_call_deinit(struct module_stack* stack, struct module_env* env)
>  {
>          int i;
> @@ -323,7 +323,7 @@ modstack_call_destartup(struct module_st
>          }
>  }
>
> -int
> +int
>  modstack_find(struct module_stack* stack, const char* name)
>  {
>         int i;
> Index: services/outside_network.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/outside_network.c,v
> diff -u -p -r1.35 outside_network.c
> --- services/outside_network.c  27 Jul 2026 14:14:39 -0000      1.35
> +++ services/outside_network.c  20 Sep 2026 09:50:48 -0000
> @@ -1702,6 +1702,12 @@ static int setup_if(struct port_if* pif,
>            !netblockstrtoaddr(addrstr, UNBOUND_DNS_PORT,
>                               &pif->addr, &pif->addrlen, &pif->pfxlen))
>                 return 0;
> +#ifdef INT_MAX
> +       if(numfd > (size_t)INT_MAX) {
> +               log_err("num_ports exceeds INT_MAX");
> +               return 0;
> +       }
> +#endif
>  #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION
>         pif->shpif = shared_ports_find_if(shp, &pif->addr, pif->addrlen,
>                 pif->pfxlen);
> @@ -1777,6 +1783,13 @@ outside_network_create(struct comm_base
>                 outside_network_delete(outnet);
>                 return NULL;
>         }
> +#ifdef INT_MAX
> +       if(num_ports > (size_t)INT_MAX) {
> +               log_err("outgoing num_ports exceeds INT_MAX");
> +               outside_network_delete(outnet);
> +               return NULL;
> +       }
> +#endif
>  #ifndef INET6
>         do_ip6 = 0;
>  #endif
> @@ -3349,9 +3362,9 @@ serviced_udp_callback(struct comm_point*
>         if(error == NETEVENT_TIMEOUT) {
>                 if(sq->status == serviced_query_UDP_EDNS && sq->last_rtt <
> 5000 &&
>                    (serviced_query_udp_size(sq,
> serviced_query_UDP_EDNS_FRAG) < serviced_query_udp_size(sq,
> serviced_query_UDP_EDNS))) {
> -                       /* fallback to 1480/1280 */
> +                       /* fallback to 1472/1232 */
>                         sq->status = serviced_query_UDP_EDNS_FRAG;
> -                       log_name_addr(VERB_ALGO, "try edns1xx0",
> sq->qbuf+10,
> +                       log_name_addr(VERB_ALGO, "try edns1xx2",
> sq->qbuf+10,
>                                 &sq->addr, sq->addrlen);
>                         if(!serviced_udp_send(sq, c->buffer)) {
>                                 serviced_callbacks(sq, NETEVENT_CLOSED, c,
> rep);
> @@ -3488,7 +3501,8 @@ outnet_serviced_query(struct outside_net
>         char* tls_auth_name, struct sockaddr_storage* addr, socklen_t
> addrlen,
>         uint8_t* zone, size_t zonelen, struct module_qstate* qstate,
>         comm_point_callback_type* callback, void* callback_arg,
> -       sldns_buffer* buff, struct module_env* env, int* was_ratelimited)
> +       sldns_buffer* buff, struct module_env* env, int* was_ratelimited,
> +       int* ratelimit_incremented)
>  {
>         struct serviced_query* sq;
>         struct service_callback* cb;
> @@ -3560,6 +3574,7 @@ outnet_serviced_query(struct outside_net
>                                         "delegation point", zone,
>                                         LDNS_RR_TYPE_NS, LDNS_RR_CLASS_IN);
>                         }
> +                       *ratelimit_incremented = 1;
>                 }
>                 /* make new serviced query entry */
>                 sq = serviced_create(outnet, buff, dnssec, want_dnssec,
> nocaps,
> @@ -3765,7 +3780,33 @@ setup_comm_ssl(struct comm_point* cp, st
>                 (void)SSL_set_tlsext_host_name(cp->ssl, host);
>         }
>  #endif
> -#ifdef HAVE_SSL_SET1_HOST
> +#ifdef HAVE_SSL_SET1_DNSNAME
> +       if((SSL_CTX_get_verify_mode(outnet->sslctx)&SSL_VERIFY_PEER)) {
> +               /* because we set SSL_VERIFY_PEER, in netevent in
> +                * ssl_handshake, it'll check if the certificate
> +                * verification has succeeded */
> +               /* SSL_VERIFY_PEER is set on the sslctx */
> +               /* and the certificates to verify with are loaded into
> +                * it with SSL_load_verify_locations or
> +                * SSL_CTX_set_default_verify_paths */
> +               /* setting the hostname makes openssl verify the
> +                * host name in the x509 certificate in the
> +                * SSL connection*/
> +               struct sockaddr_storage tmpaddr;
> +               socklen_t tmpaddrlen = (socklen_t)sizeof(tmpaddr);
> +               if(ipstrtoaddr(host, UNBOUND_DNS_PORT, &tmpaddr,
> &tmpaddrlen)) {
> +                       if(!SSL_set1_ipaddr(cp->ssl, host)) {
> +                               log_err("SSL_set1_ipaddr failed");
> +                               return 0;
> +                       }
> +               } else {
> +                       if(!SSL_set1_dnsname(cp->ssl, host)) {
> +                               log_err("SSL_set1_dnsname failed");
> +                               return 0;
> +                       }
> +               }
> +       }
> +#elif defined(HAVE_SSL_SET1_HOST)
>         if((SSL_CTX_get_verify_mode(outnet->sslctx)&SSL_VERIFY_PEER)) {
>                 /* because we set SSL_VERIFY_PEER, in netevent in
>                  * ssl_handshake, it'll check if the certificate
> @@ -3894,7 +3935,8 @@ outnet_comm_point_for_http(struct outsid
>                 /* outnet_tcp_connect has closed fd on error for us */
>                 return 0;
>         }
> -       cp = comm_point_create_http_out(outnet->base, 65552, cb, cb_arg,
> +       cp = comm_point_create_http_out(outnet->base,
> +               sldns_buffer_capacity(outnet->udp_buff), cb, cb_arg,
>                 outnet->udp_buff);
>         if(!cp) {
>                 log_err("malloc failure");
> @@ -4085,13 +4127,15 @@ static int shared_ports_alloc_ifs(struct
>                 size_t done_4 = 0, done_6 = 0;
>                 int i;
>                 for(i=0; i<num_ifs; i++) {
> -                       if(str_is_ip6(ifs[i]) && do_ip6) {
> +                       if(str_is_ip6(ifs[i]) && do_ip6 &&
> +                               (int)done_6 < shp->num_ip6) {
>
> if(!shared_ports_setup_if(&shp->ip6_ifs[done_6],
>                                         ifs[i], availports, numavailports))
>                                         return 0;
>                                 done_6++;
>                         }
> -                       if(!str_is_ip6(ifs[i]) && do_ip4) {
> +                       if(!str_is_ip6(ifs[i]) && do_ip4 &&
> +                               (int)done_4 < shp->num_ip4) {
>
> if(!shared_ports_setup_if(&shp->ip4_ifs[done_4],
>                                         ifs[i], availports, numavailports))
>                                         return 0;
> @@ -4112,16 +4156,21 @@ struct shared_ports* shared_ports_create
>                 return NULL;
>         }
>         lock_basic_init(&shp->lock);
> -       lock_protect(&shp->lock, shp, sizeof(*shp));
> +       lock_protect(&shp->lock, &shp->ip4_ifs, sizeof(shp->ip4_ifs));
> +       lock_protect(&shp->lock, &shp->num_ip4, sizeof(shp->num_ip4));
> +       lock_protect(&shp->lock, &shp->ip6_ifs, sizeof(shp->ip6_ifs));
> +       lock_protect(&shp->lock, &shp->num_ip6, sizeof(shp->num_ip6));
>
>  #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION
>         /* Allocate interfaces */
> +       lock_basic_lock(&shp->lock);
>         if(!shared_ports_alloc_ifs(shp, ifs, num_ifs, do_ip4, do_ip6,
>                 availports, numavailports)) {
>                 log_err("malloc failed");
>                 shared_ports_delete(shp);
>                 return NULL;
>         }
> +       lock_basic_unlock(&shp->lock);
>  #else
>         (void)ifs; (void)num_ifs; (void)do_ip4; (void)do_ip6;
>         (void)availports; (void)numavailports;
> @@ -4199,6 +4248,9 @@ int shared_ports_fetch_random(struct sha
>         int portno = 0, my_port = 0;
>         if(!shpif)
>                 return 0;
> +#  ifdef THREADS_DISABLED
> +       (void)shp;
> +#  endif
>         lock_basic_lock(&shp->lock);
>         if(udp_connect) {
>                 /* if we connect() we cannot reuse fds for a port. */
> @@ -4256,6 +4308,9 @@ void shared_ports_return_port(struct sha
>  #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION
>         if(!shpif)
>                 return;
> +#  ifdef THREADS_DISABLED
> +       (void)shp;
> +#  endif
>         lock_basic_lock(&shp->lock);
>         log_assert(shpif->inuse > 0);
>         shpif->avail_ports[shpif->avail_total - shpif->inuse] = port;
> Index: services/outside_network.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/outside_network.h,v
> diff -u -p -r1.19 outside_network.h
> --- services/outside_network.h  27 Jul 2026 14:14:39 -0000      1.19
> +++ services/outside_network.h  20 Sep 2026 09:50:48 -0000
> @@ -538,7 +538,7 @@ struct serviced_query {
>                 serviced_query_UDP_EDNS_fallback,
>                 /** probe to test TCP noEDNS0 (EDNS gives FORMERRorNOTIMP)
> */
>                 serviced_query_TCP_EDNS_fallback,
> -               /** send UDP query with EDNS1480 (or 1280) */
> +               /** send UDP query with EDNS1472 (or 1232) */
>                 serviced_query_UDP_EDNS_FRAG
>         }
>                 /** variable with current status */
> @@ -697,6 +697,8 @@ void pending_delete(struct outside_netwo
>   * @param env: the module environment.
>   * @param was_ratelimited: it will signal back if the query failed to
> pass the
>   *     ratelimit check.
> + * @param ratelimit_incremented: set to true if the ratelimit counter
> + *     was increased.
>   * @return 0 on error, or pointer to serviced query that is used to answer
>   *     this serviced query may be shared with other callbacks as well.
>   */
> @@ -706,7 +708,8 @@ struct serviced_query* outnet_serviced_q
>         char* tls_auth_name, struct sockaddr_storage* addr, socklen_t
> addrlen,
>         uint8_t* zone, size_t zonelen, struct module_qstate* qstate,
>         comm_point_callback_type* callback, void* callback_arg,
> -       struct sldns_buffer* buff, struct module_env* env, int*
> was_ratelimited);
> +       struct sldns_buffer* buff, struct module_env* env, int*
> was_ratelimited,
> +       int* ratelimit_incremented);
>
>  /**
>   * Remove service query callback.
> Index: services/rpz.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/rpz.c,v
> diff -u -p -r1.1.1.17 rpz.c
> --- services/rpz.c      26 May 2026 11:10:47 -0000      1.1.1.17
> +++ services/rpz.c      20 Sep 2026 09:50:48 -0000
> @@ -721,13 +721,22 @@ rpz_insert_local_zones_trigger(struct lo
>                 char* rrstr = sldns_wire2str_rr(rr, rr_len);
>                 if(rrstr == NULL) {
>                         log_err("malloc error while inserting rpz nsdname
> trigger");
> -                       free(dname);
> +                       if(!newzone)
> +                               free(dname);
>                         lock_rw_unlock(&lz->lock);
>                         return;
>                 }
>                 lock_rw_wrlock(&z->lock);
> -               local_zone_enter_rr(z, dname, dnamelen, dnamelabs, rrtype,
> -                                   rrclass, ttl, rdata, rdata_len, rrstr);
> +               if(!local_zone_enter_rr(z, dname, dnamelen, dnamelabs,
> rrtype,
> +                                   rrclass, ttl, rdata, rdata_len,
> rrstr)) {
> +                       log_err("rpz: could not enter local-data: %s",
> rrstr);
> +                       if(!newzone)
> +                               free(dname);
> +                       lock_rw_unlock(&z->lock);
> +                       lock_rw_unlock(&lz->lock);
> +                       free(rrstr);
> +                       return;
> +               }
>                 lock_rw_unlock(&z->lock);
>                 free(rrstr);
>         }
> @@ -805,8 +814,9 @@ rpz_insert_nsdname_trigger(struct rpz* r
>         uint8_t* dname_stripped = NULL;
>         size_t dnamelen_stripped = 0;
>
> -       rpz_strip_nsdname_suffix(dname, dnamelen, &dname_stripped,
> -               &dnamelen_stripped);
> +       if(!rpz_strip_nsdname_suffix(dname, dnamelen, &dname_stripped,
> +               &dnamelen_stripped))
> +               return;
>         if(a == RPZ_INVALID_ACTION) {
>                 verbose(VERB_ALGO, "rpz: skipping invalid action");
>                 free(dname_stripped);
> @@ -904,8 +914,8 @@ rpz_report_rrset_error(const char* msg,
>
>  /* from localzone.c; difference is we don't have a dname */
>  static struct local_rrset*
> -rpz_clientip_new_rrset(struct regional* region,
> -       struct clientip_synthesized_rr* raddr, uint16_t rrtype, uint16_t
> rrclass)
> +rpz_clientip_new_rrset(struct regional* region, uint16_t rrtype,
> +       uint16_t rrclass)
>  {
>         struct packed_rrset_data* pd;
>         struct local_rrset* rrset = (struct local_rrset*)
> @@ -914,8 +924,6 @@ rpz_clientip_new_rrset(struct regional*
>                 log_err("out of memory");
>                 return NULL;
>         }
> -       rrset->next = raddr->data;
> -       raddr->data = rrset;
>         rrset->rrset = (struct ub_packed_rrset_key*)
>                 regional_alloc_zero(region, sizeof(*rrset->rrset));
>         if(rrset->rrset == NULL) {
> @@ -954,12 +962,18 @@ rpz_clientip_enter_rr(struct regional* r
>                 return 0;
>         }
>
> -       rrset = rpz_clientip_new_rrset(region, raddr, rrtype, rrclass);
> -       if(raddr->data == NULL) {
> +       rrset = rpz_clientip_new_rrset(region, rrtype, rrclass);
> +       if(rrset == NULL) {
>                 return 0;
>         }
>
> -       return rrset_insert_rr(region, rrset->rrset->entry.data, rdata,
> rdata_len, ttl, "");
> +       if(!rrset_insert_rr(region, rrset->rrset->entry.data, rdata,
> rdata_len, ttl, ""))
> +               return 0;
> +
> +       /* Link in now that the allocations have succeeded. */
> +       rrset->next = raddr->data;
> +       raddr->data = rrset;
> +       return 1;
>  }
>
>  static int
> @@ -982,7 +996,6 @@ rpz_clientip_insert_trigger_rr(struct cl
>         lock_rw_wrlock(&node->lock);
>         lock_rw_unlock(&set->lock);
>
> -       node->action = a;
>         if(a == RPZ_LOCAL_DATA_ACTION) {
>                 if(!rpz_clientip_enter_rr(set->region, node, rrtype,
>                         rrclass, ttl, rdata, rdata_len)) {
> @@ -992,6 +1005,7 @@ rpz_clientip_insert_trigger_rr(struct cl
>                 }
>
>         }
> +       node->action = a;
>
>         lock_rw_unlock(&node->lock);
>
> @@ -1977,8 +1991,9 @@ rpz_synthesize_nodata(struct rpz* ATTR_U
>                                              0, /* total */
>                                              sec_status_insecure,
>                                              LDNS_EDE_NONE);
> -       if(msg->rep)
> -               msg->rep->authoritative = 1;
> +       if(!msg->rep)
> +               return NULL;
> +       msg->rep->authoritative = 1;
>         if(!rpz_add_soa(msg->rep, ms, az))
>                 return NULL;
>         return msg;
> @@ -2008,8 +2023,9 @@ rpz_synthesize_nxdomain(struct rpz* r, s
>                                              0, /* total */
>                                              sec_status_insecure,
>                                              LDNS_EDE_NONE);
> -       if(msg->rep)
> -               msg->rep->authoritative = 1;
> +       if(!msg->rep)
> +               return NULL;
> +       msg->rep->authoritative = 1;
>         if(!rpz_add_soa(msg->rep, ms, az))
>                 return NULL;
>         return msg;
> Index: services/cache/dns.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/cache/dns.c,v
> diff -u -p -r1.29 dns.c
> --- services/cache/dns.c        27 Jul 2026 14:14:39 -0000      1.29
> +++ services/cache/dns.c        20 Sep 2026 09:50:48 -0000
> @@ -43,6 +43,7 @@
>  #include "iterator/iter_utils.h"
>  #include "validator/val_nsec.h"
>  #include "validator/val_utils.h"
> +#include "iterator/iter_utils.h"
>  #include "services/cache/dns.h"
>  #include "services/cache/rrset.h"
>  #include "util/data/msgparse.h"
> @@ -131,8 +132,8 @@ store_rrsets(struct module_env* env, str
>                                 rep->ref[i].id == rep->ref[i].key->id) {
>                                 ttl = ((struct packed_rrset_data*)
>                                     rep->rrsets[i]->entry.data)->ttl;
> -               if(ttl < min_ttl) min_ttl = ttl;
> -       }
> +                               if(ttl < min_ttl) min_ttl = ttl;
> +                       }
>                         lock_rw_unlock(&rep->ref[i].key->entry.lock);
>                 }
>         }
> @@ -586,8 +587,12 @@ dns_cache_find_delegation(struct module_
>                         return NULL;
>                 }
>         }
> -       if(!delegpt_rrset_add_ns(dp, region, nskey, 0))
> +       if(!delegpt_rrset_add_ns(dp, region, nskey, 0,
> +               deleg_port_number(env))) {
> +               lock_rw_unlock(&nskey->entry.lock);
>                 log_err("find_delegation: addns out of memory");
> +               return NULL;
> +       }
>         lock_rw_unlock(&nskey->entry.lock); /* first unlock before next
> lookup*/
>         /* find and add DS/NSEC (if any) */
>         if(msg)
> @@ -672,7 +677,7 @@ tomsg(struct module_env* env, struct que
>                 rrset_array_unlock(r->ref, r->rrset_count);
>                 return NULL;
>         }
> -       if(r->security == sec_status_secure &&
> !reply_all_rrsets_secure(r)) {
> +       if(r->security == sec_status_secure &&
> !reply_an_ns_rrsets_secure(r)) {
>                 /* message rrsets have changed status, revalidate */
>                 rrset_array_unlock(r->ref, r->rrset_count);
>                 return NULL;
> @@ -782,11 +787,16 @@ synth_dname_msg(struct ub_packed_rrset_k
>         uint8_t* newname, *dtarg = NULL;
>         size_t newlen, dtarglen;
>         time_t rr_ttl;
> +       int graceperiod = 0;
>         if(TTL_IS_EXPIRED(d->ttl, now)) {
>                 /* Allow TTL=0 DNAME from upstream within grace period */
>                 if(!(rrset->rk.flags & PACKED_RRSET_UPSTREAM_0TTL))
>                         return NULL;
>                 rr_ttl = 0;
> +               /* Since PACKED_RRSET_UPSTREAM_0TTL set the flag that
> +                * the grace period has been applied, this stops the rrset
> +                * from getting stored back into the cache with a bigger
> TTL.*/
> +               graceperiod = 1;
>         } else {
>                 rr_ttl = d->ttl - now;
>         }
> @@ -814,6 +824,8 @@ synth_dname_msg(struct ub_packed_rrset_k
>         msg->rep->rrsets[0] = packed_rrset_copy_region(rrset, region, now);
>         if(!msg->rep->rrsets[0]) /* copy DNAME */
>                 return NULL;
> +       if(graceperiod)
> +               msg->rep->rrsets[0]->rk.flags |= PACKED_RRSET_0TTL_GRACE;
>         /* synth CNAME rrset */
>         get_cname_target(rrset, &dtarg, &dtarglen);
>         if(!dtarg)
> Index: services/cache/rrset.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/services/cache/rrset.c,v
> diff -u -p -r1.12 rrset.c
> --- services/cache/rrset.c      27 Jul 2026 14:14:39 -0000      1.12
> +++ services/cache/rrset.c      20 Sep 2026 09:50:48 -0000
> @@ -215,6 +215,13 @@ rrset_cache_update(struct rrset_cache* r
>         int equal = 0;
>         log_assert(ref->id != 0 && k->id != 0);
>         log_assert(k->rk.dname != NULL);
> +       if((k->rk.flags&PACKED_RRSET_0TTL_GRACE) !=0) {
> +               log_nametypeclass(VERB_ALGO, "rrset store of
> PACKED_RRSET_0TTL_GRACE rrset skipped", k->rk.dname, rrset_type,
> ntohs(k->rk.rrset_class));
> +               ub_packed_rrset_parsedelete(k, alloc);
> +               return 0; /* Do not store 0TTL items after apply of
> +                       the grace ttl amount.
> +                       This means the ref was not changed by the call. */
> +       }
>         /* looks up item with a readlock - no editing! */
>         if((e=slabhash_lookup(&r->table, h, k, 0)) != 0) {
>                 /* return id and key as they will be used in the cache
> @@ -291,6 +298,8 @@ void rrset_cache_update_wildcard(struct
>  {
>         struct rrset_ref ref;
>         uint8_t wc_dname[LDNS_MAX_DOMAINLEN+3];
> +       uint8_t* new_dname;
> +       size_t new_dname_len;
>
>         /* See if the RRSIG signer name allows this wildcard,
>          * the new rrset should fall within the zone of the RRSIG
> signer(s). */
> @@ -310,14 +319,16 @@ void rrset_cache_update_wildcard(struct
>         wc_dname[1] = (uint8_t)'*';
>         memmove(wc_dname+2, ce, ce_len);
>
> -       free(rrset->rk.dname);
> -       rrset->rk.dname_len = ce_len + 2;
> -       rrset->rk.dname = (uint8_t*)memdup(wc_dname, rrset->rk.dname_len);
> -       if(!rrset->rk.dname) {
> -               alloc_special_release(alloc, rrset);
> +       new_dname_len = ce_len + 2;
> +       new_dname = (uint8_t*)memdup(wc_dname, new_dname_len);
> +       if(!new_dname) {
> +               ub_packed_rrset_parsedelete(rrset, alloc);
>                 log_err("memdup failure in rrset_cache_update_wildcard");
>                 return;
>         }
> +       free(rrset->rk.dname);
> +       rrset->rk.dname = new_dname;
> +       rrset->rk.dname_len = new_dname_len;
>
>         rrset->entry.hash = rrset_key_hash(&rrset->rk);
>         ref.key = rrset;
> Index: sldns/keyraw.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/sldns/keyraw.c,v
> diff -u -p -r1.11 keyraw.c
> --- sldns/keyraw.c      26 Sep 2025 07:32:37 -0000      1.11
> +++ sldns/keyraw.c      20 Sep 2026 09:50:48 -0000
> @@ -67,19 +67,28 @@ sldns_rr_dnskey_key_size_raw(const unsig
>         case LDNS_RSASHA512:
>  #endif
>                 if (len > 0) {
> +                       size_t nlen, offset;
>                         if (keydata[0] == 0) {
>                                 /* big exponent */
>                                 if (len > 3) {
>                                         memmove(&int16, keydata + 1, 2);
>                                         exp = ntohs(int16);
> -                                       return (len - exp - 3)*8;
> +                                       offset = 3;
>                                 } else {
>                                         return 0;
>                                 }
>                         } else {
>                                 exp = keydata[0];
> -                               return (len-exp-1)*8;
> +                               offset = 1;
>                         }
> +                       if(exp+offset > len)
> +                               return 0;
> +                       nlen = len - exp - offset;
> +                       /* prefixed zeroes mean a smaller value */
> +                       while(nlen > 0 &&
> +                               keydata[len-nlen] == 0)
> +                               nlen--;
> +                       return nlen*8;
>                 } else {
>                         return 0;
>                 }
> Index: sldns/str2wire.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/sldns/str2wire.c,v
> diff -u -p -r1.19 str2wire.c
> --- sldns/str2wire.c    26 Sep 2025 07:32:37 -0000      1.19
> +++ sldns/str2wire.c    20 Sep 2026 09:50:48 -0000
> @@ -842,7 +842,8 @@ rrinternal_parse_rdata(sldns_buffer* str
>         sldns_write_uint16(rr+dname_len+8,
> (uint16_t)(rr_cur_len-dname_len-10));
>         *rr_len = rr_cur_len;
>         /* SVCB/HTTPS handling  */
> -       if (rr_type == LDNS_RR_TYPE_SVCB || rr_type == LDNS_RR_TYPE_HTTPS)
> {
> +       if ((rr_type == LDNS_RR_TYPE_SVCB || rr_type == LDNS_RR_TYPE_HTTPS)
> +               && !was_unknown_rr_format) {
>                 size_t rdata_len = rr_cur_len - dname_len - 10;
>                 uint8_t *rdata = rr+dname_len + 10;
>
> @@ -1201,7 +1202,7 @@ sldns_str2wire_svcbparam_ipv4hint(const
>  {
>         size_t count;
>         char ip_str[INET_ADDRSTRLEN+1];
> -       char *next_ip_str;
> +       const char *next_ip_str;
>         size_t i;
>
>         for (i = 0, count = 1; val[i]; i++) {
> @@ -1256,7 +1257,7 @@ sldns_str2wire_svcbparam_ipv6hint(const
>  {
>         size_t count;
>         char ip_str[INET6_ADDRSTRLEN+1];
> -       char *next_ip_str;
> +       const char *next_ip_str;
>         size_t i;
>
>         for (i = 0, count = 1; val[i]; i++) {
> @@ -1317,7 +1318,7 @@ static int
>  sldns_str2wire_svcbparam_mandatory(const char* val, uint8_t* rd, size_t*
> rd_len)
>  {
>         size_t i, count, val_len;
> -       char* next_key;
> +       const char* next_key;
>
>         val_len = strlen(val);
>
> @@ -1410,6 +1411,7 @@ sldns_str2wire_svcbparam_ech_value(const
>                         return LDNS_WIREPARSE_ERR_BUFFER_TOO_SMALL;
>                 sldns_write_uint16(rd, SVCB_KEY_ECH);
>                 sldns_write_uint16(rd + 2, 0);
> +               *rd_len = 4;
>
>                 return LDNS_WIREPARSE_ERR_OK;
>         }
> Index: smallapp/unbound-anchor.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/smallapp/unbound-anchor.c,v
> diff -u -p -r1.23 unbound-anchor.c
> --- smallapp/unbound-anchor.c   26 May 2026 11:14:11 -0000      1.23
> +++ smallapp/unbound-anchor.c   20 Sep 2026 09:50:48 -0000
> @@ -160,7 +160,7 @@ char* wsa_strerror(int err);
>  #endif
>
>  static const char ICANN_UPDATE_CA[] =
> -       /* The ICANN CA fetched at 24 Sep 2010.  Valid to 2028 */
> +       /* The ICANN CA fetched at 29 May 2026. Valid to 20 Mar 2045 */
>         "-----BEGIN CERTIFICATE-----\n"
>
> "MIIDdzCCAl+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBdMQ4wDAYDVQQKEwVJQ0FO\n"
>
> "TjEmMCQGA1UECxMdSUNBTk4gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNV\n"
> @@ -181,6 +181,40 @@ static const char ICANN_UPDATE_CA[] =
>
> "15nu5JBSewrCkYqYYmaxyOC3WrVGfHZxVI7MpIFcGdvSb2a1uyuua8l0BKgk3ujF\n"
>
> "0/wsHNeP22qNyVO+XVBzrM8fk8BSUFuiT/6tZTYXRtEt5aKQZgXbKU5dUF3jT9qg\n"
>         "j/Br5BZw3X/zd325TvnswzMC1+ljLzHnQGGk\n"
> +       "-----END CERTIFICATE-----\n"
> +       "\n"
> +       "-----BEGIN CERTIFICATE-----\n"
> +
>  "MIIFsTCCA5mgAwIBAgIUQFsYkgroBoe69HKQPy8/DQuiLwgwDQYJKoZIhvcNAQEN\n"
> +
>  "BQAwYDELMAkGA1UEBhMCVVMxDjAMBgNVBAoMBUlDQU5OMSYwJAYDVQQLDB1JQ0FO\n"
> +
>  "TiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEZMBcGA1UEAwwQSUNBTk4gUm9vdCBD\n"
> +
>  "QSB2MjAeFw0yNTAzMjAyMTA0MjZaFw00NTAzMjAyMTA0MjZaMGAxCzAJBgNVBAYT\n"
> +
>  "AlVTMQ4wDAYDVQQKDAVJQ0FOTjEmMCQGA1UECwwdSUNBTk4gQ2VydGlmaWNhdGlv\n"
> +
>  "biBBdXRob3JpdHkxGTAXBgNVBAMMEElDQU5OIFJvb3QgQ0EgdjIwggIiMA0GCSqG\n"
> +
>  "SIb3DQEBAQUAA4ICDwAwggIKAoICAQCepDjrubjR7en/uZWo7MAnzFIIvUPYEc7b\n"
> +
>  "+AlefdlEDQ1JEmpfrvt/4CX9lJ9ShIBR6zwrQeDvrj5XZ2kEjbJ8Nnc6sM/ojdyr\n"
> +
>  "5jSLqcDPH9fJg7jCW02KF8CtqWsnqcW6jjTIZcCWkg9lEixdF8QAjIEgJtZte+Yh\n"
> +
>  "XeyN0KD2EaO8U5Id0bLvMyphuO1OCGKzDtetcX8K7SvoshdJx3lPIlYzqXl0nVAY\n"
> +
>  "iCeNdeDzTNjEOHYJOP6dYoZI8nKRJltMkZcCCjBE2vQuSMY2w4pOlWk1skHjMWXj\n"
> +
>  "QsZzngXuNG56zialL0TPEDVWjWRjzOnruHUAs4KUY8Zs+Nt8JdSlXMi825PKoKpp\n"
> +
>  "ESs7/ZG1mPjVOYp7Z7ntrRjJFgnUBjWzVPOx4yHiJj1ur+OpqP18oP5YfqY+tKmz\n"
> +
>  "7vlfRGGOEd08a0XgZISDNKpMAovn5pRUHTWPCCjc28tns9ODPvr1cQi+QSwTv+v8\n"
> +
>  "wnA5etGrsead88Rv/ieaq5ikMJTRDfW4d9SY2uPcMGvfU6VdQLRhQkzEVTQNAJ1R\n"
> +
>  "i2lOoJbbjwnK+OU9OhST/OqdjJDJAhTAstdUnrr8WBU80xM75MIaaTjSBCvZ1wro\n"
> +
>  "pAi2hYb0tedTH6WarSW3MH9HcEoGGzs2GD3hDB0a2eCp+TdAs8Up944SjY7UV4Jx\n"
> +
>  "sOC7TxbmkQIDAQABo2MwYTAdBgNVHQ4EFgQU+1EuMRuOZ/ecsfYzNQ+yGZsxZrMw\n"
> +
>  "HwYDVR0jBBgwFoAU+1EuMRuOZ/ecsfYzNQ+yGZsxZrMwDwYDVR0TAQH/BAUwAwEB\n"
> +
>  "/zAOBgNVHQ8BAf8EBAMCAf4wDQYJKoZIhvcNAQENBQADggIBACz38SkKR1WsEZnX\n"
> +
>  "x1BKaS5/oQPw+7quDQCKGoD2Vz7CR7yQh4zQn/Hh0173vKvRWcwN2io0iLJ1ysv5\n"
> +
>  "jXBLeWZh3djiQlXP3iWp4s01SiUwmFssxi3SD1IT2jNosk1xcVWthle9zth7Y8Mp\n"
> +
>  "iUJYnHobP7tX7H2g+I8Rqw2sEX/yPSYMYcdH5a1xRMPOLHTyOaCgevRBBBtXkiAJ\n"
> +
>  "Ob9QKZTaFaXntPXBKNSGkVb2d+2qKyJMrwd0KNI+SVSoIgNDAxkNOdi9x6X6ETW2\n"
> +
>  "4aYFsytohFVkNUXx2eFYRim4yjnD8PHIvDQSofLfSAC5TOERtwUFd+Mw3/di+HCm\n"
> +
>  "50OJPyoxZLjWQCCfNUZzgZZOe+zT6lgBiV3KB0UuuAdq7jGUeH/328HJDi30BvNj\n"
> +
>  "+TNb9Hmpm+ZDguM+f8p7GxapX8AVNu/xErtl4msYiVJrr1qqV+qLLEMwIz0raujG\n"
> +
>  "FFDd6N43wgduffbU20pThry0Y7rku5+RZjUZe/T7ZL+NUKiqXAPufrkqVkjX/8T+\n"
> +
>  "wyNZz8KkiQwkJthojpppa79FDxn/A2M8tt+FQqIONAUPR2m5nurVgftQH0z5ZtDB\n"
> +
>  "YykUlkUiPOJNXoDOIkbpA7lW2wezeY4te+EiSeUZSE541N5QBwaItaonIZsIgn6C\n"
> +       "pMnwChV9468oRE20bdqq9+Go7g4E\n"
>         "-----END CERTIFICATE-----\n";
>
>  static const char DS_TRUST_ANCHOR[] =
> @@ -1678,18 +1712,116 @@ static unsigned long
>  get_usage_of_ex(X509* cert)
>  {
>         unsigned long val = 0;
> +#ifdef HAVE_X509_GET_KEY_USAGE
> +       val = X509_get_key_usage(cert);
> +       if (val == UINT32_MAX)
> +               return 0;
> +#else
>         ASN1_BIT_STRING* s;
>         if((s=X509_get_ext_d2i(cert, NID_key_usage, NULL, NULL))) {
> -               if(s->length > 0) {
> -                       val = s->data[0];
> -                       if(s->length > 1)
> -                               val |= s->data[1] << 8;
> +#  ifdef HAVE_ASN1_STRING_GET0_DATA
> +               const unsigned char *data = ASN1_STRING_get0_data(s);
> +#  else
> +               const unsigned char *data = ASN1_STRING_data(s);
> +#  endif
> +               int len = ASN1_STRING_length(s);
> +               if(len > 0) {
> +                       val = data[0];
> +                       if(len > 1)
> +                               val |= data[1] << 8;
>                 }
>                 ASN1_BIT_STRING_free(s);
>         }
> +#endif
>         return val;
>  }
>
> +#if !defined(HAVE_X509_NAME_GET_TEXT_BY_NID) ||
> defined(DEPRECATED_X509_NAME_GET_TEXT_BY_NID)
> +/** print verbose output about name extension data. */
> +static void
> +print_name_ext(
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +       const
> +#endif
> +       X509_NAME* nm, int nid, const char* str)
> +{
> +       int lastpos = -1;
> +       for(;;) {
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +               const
> +#endif
> +               X509_NAME_ENTRY* ne;
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +               const
> +#endif
> +               ASN1_STRING *asn;
> +               const unsigned char *data;
> +               char buf[1024];
> +
> +               lastpos = X509_NAME_get_index_by_NID(nm, nid, lastpos);
> +               if(lastpos == -1 || lastpos == -2)
> +                       break;
> +               ne = X509_NAME_get_entry(nm, lastpos);
> +               if(!ne) continue;
> +               asn = X509_NAME_ENTRY_get_data(ne);
> +               if(!asn) continue;
> +#  ifdef HAVE_ASN1_STRING_GET0_DATA
> +               data = ASN1_STRING_get0_data(asn);
> +#  else
> +               data = ASN1_STRING_data(asn);
> +#  endif
> +               if(!data) continue;
> +               if(ASN1_STRING_length(asn) > (int)sizeof(buf)-1) continue;
> +               memcpy(buf, data, ASN1_STRING_length(asn));
> +               buf[ASN1_STRING_length(asn)]=0;
> +               printf("%s: %s\n", str, buf);
> +       }
> +}
> +#endif /* X509_NAME_GET_TEXT_BY_NID */
> +
> +#if !defined(HAVE_X509_NAME_GET_TEXT_BY_NID) ||
> defined(DEPRECATED_X509_NAME_GET_TEXT_BY_NID)
> +/** see if the valid emailaddr is present. */
> +static int
> +has_valid_emailaddr(
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +       const
> +#endif
> +       X509_NAME* nm, const char* p7signer)
> +{
> +       int lastpos = -1;
> +       for(;;) {
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +               const
> +#endif
> +               X509_NAME_ENTRY* ne;
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +               const
> +#endif
> +               ASN1_STRING *asn;
> +               const unsigned char *data;
> +
> +               lastpos = X509_NAME_get_index_by_NID(nm,
> +                       NID_pkcs9_emailAddress, lastpos);
> +               if(lastpos == -1 || lastpos == -2)
> +                       break;
> +               ne = X509_NAME_get_entry(nm, lastpos);
> +               if(!ne) continue;
> +               asn = X509_NAME_ENTRY_get_data(ne);
> +               if(!asn) continue;
> +#  ifdef HAVE_ASN1_STRING_GET0_DATA
> +               data = ASN1_STRING_get0_data(asn);
> +#  else
> +               data = ASN1_STRING_data(asn);
> +#  endif
> +               if(!data) continue;
> +               if(ASN1_STRING_length(asn) == (int)strlen(p7signer) &&
> +                       strncmp((char*)data, p7signer, strlen(p7signer))
> == 0)
> +                       return 1; /* match */
> +       }
> +       return 0;
> +}
> +#endif /* X509_NAME_GET_TEXT_BY_NID */
> +
>  /** get valid signers from the list of signers in the signature */
>  static STACK_OF(X509)*
>  get_valid_signers(PKCS7* p7, const char* p7signer)
> @@ -1709,6 +1841,9 @@ get_valid_signers(PKCS7* p7, const char*
>                 return NULL;
>         }
>         for(i=0; i<sk_X509_num(signers); i++) {
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +               const
> +#endif
>                 X509_NAME* nm = X509_get_subject_name(
>                         sk_X509_value(signers, i));
>                 char buf[1024];
> @@ -1721,17 +1856,29 @@ get_valid_signers(PKCS7* p7, const char*
>                                 (int)sizeof(buf));
>                         printf("signer %d: Subject: %s\n", i,
>                                 nmline?nmline:"no subject");
> +#if !defined(HAVE_X509_NAME_GET_TEXT_BY_NID) ||
> defined(DEPRECATED_X509_NAME_GET_TEXT_BY_NID)
> +                       if(verb >= 3) {
> +                               print_name_ext(nm, NID_commonName,
> +                                       "commonName");
> +                               print_name_ext(nm, NID_pkcs9_emailAddress,
> +                                       "emailAddress");
> +                       }
> +#else
>                         if(verb >= 3 && X509_NAME_get_text_by_NID(nm,
> -                               NID_commonName, buf, (int)sizeof(buf)))
> +                               NID_commonName, buf, (int)sizeof(buf)) > 0)
>                                 printf("commonName: %s\n", buf);
>                         if(verb >= 3 && X509_NAME_get_text_by_NID(nm,
> -                               NID_pkcs9_emailAddress, buf,
> (int)sizeof(buf)))
> +                               NID_pkcs9_emailAddress, buf,
> (int)sizeof(buf)) > 0)
>                                 printf("emailAddress: %s\n", buf);
> +#endif
>                 }
>                 if(verb) {
>                         int ku_loc = X509_get_ext_by_NID(
>                                 sk_X509_value(signers, i), NID_key_usage,
> -1);
>                         if(verb >= 3 && ku_loc >= 0) {
> +#if OPENSSL_VERSION_NUMBER >= 0x40000000
> +                               const
> +#endif
>                                 X509_EXTENSION *ex = X509_get_ext(
>                                         sk_X509_value(signers, i), ku_loc);
>                                 if(ex) {
> @@ -1745,16 +1892,23 @@ get_valid_signers(PKCS7* p7, const char*
>                         /* there is no name to check, return all records */
>                         if(verb) printf("did not check commonName of
> signer\n");
>                 } else {
> -                       if(!X509_NAME_get_text_by_NID(nm,
> +#if !defined(HAVE_X509_NAME_GET_TEXT_BY_NID) ||
> defined(DEPRECATED_X509_NAME_GET_TEXT_BY_NID)
> +                       if(!has_valid_emailaddr(nm, p7signer)) {
> +                               if(verb) printf("removed cert with wrong
> emailaddress\n");
> +                               continue; /* wrong name, skip it */
> +                       }
> +#else
> +                       if(X509_NAME_get_text_by_NID(nm,
>                                 NID_pkcs9_emailAddress,
> -                               buf, (int)sizeof(buf))) {
> -                               if(verb) printf("removed cert with no
> name\n");
> +                               buf, (int)sizeof(buf)) <= 0) {
> +                               if(verb) printf("removed cert with no
> emailaddress\n");
>                                 continue; /* no name, no use */
>                         }
>                         if(strcmp(buf, p7signer) != 0) {
> -                               if(verb) printf("removed cert with wrong
> name\n");
> +                               if(verb) printf("removed cert with wrong
> emailaddress\n");
>                                 continue; /* wrong name, skip it */
>                         }
> +#endif
>                 }
>
>                 /* check that the key usage allows digital signatures
> Index: smallapp/unbound-checkconf.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/smallapp/unbound-checkconf.c,v
> diff -u -p -r1.28 unbound-checkconf.c
> --- smallapp/unbound-checkconf.c        26 May 2026 11:14:11 -0000
> 1.28
> +++ smallapp/unbound-checkconf.c        20 Sep 2026 09:50:48 -0000
> @@ -73,6 +73,9 @@
>  #ifdef HAVE_GLOB_H
>  #include <glob.h>
>  #endif
> +#ifdef HAVE_FNMATCH_H
> +#include <fnmatch.h>
> +#endif
>  #ifdef WITH_PYTHONMODULE
>  #include "pythonmod/pythonmod.h"
>  #endif
> @@ -728,6 +731,122 @@ check_modules_exist(const char* module_c
>         }
>  }
>
> +#ifdef USE_IPSECMOD
> +/** Compare filename with string, true if it matches the name. */
> +static int
> +file_string_matches(char* str, char* fname, struct config_file* cfg)
> +{
> +       char* f;
> +       if(!str || str[0] == 0)
> +               return 0;
> +       /* compare name after chroot and working dir are applied */
> +       f = fname_after_chroot(str, cfg, 1);
> +       if(!f) fatal_exit("out of memory");
> +       if(strcmp(fname, f) == 0) {
> +               free(f);
> +               return 1;
> +       }
> +       free(f);
> +       return 0;
> +}
> +#endif /* USE_IPSECMOD */
> +
> +/** Compare filename with list of files, true if list contains the name.
> */
> +static int
> +file_list_contains(struct config_strlist* list, char* fname,
> +       struct config_file* cfg)
> +{
> +       struct config_strlist* s;
> +       char* f;
> +       for(s = list; s; s = s->next) {
> +               if(!s->str || s->str[0] == 0)
> +                       continue; /* skip if no file name */
> +               /* compare names after chroot and working dir are applied
> */
> +               f = fname_after_chroot(s->str, cfg, 1);
> +               if(!f) fatal_exit("out of memory");
> +               if(strcmp(fname, f) == 0) {
> +                       free(f);
> +                       return 1;
> +               }
> +               free(f);
> +       }
> +       return 0;
> +}
> +
> +/** Compare filename with list of files, true if list contains the name,
> + * with glob compare. */
> +static int
> +file_list_contains_wild(struct config_strlist* list, char* fname,
> +       struct config_file* cfg)
> +{
> +       struct config_strlist* s;
> +       char* f;
> +       for(s = list; s; s = s->next) {
> +               if(!s->str || s->str[0] == 0)
> +                       continue; /* skip if no file name */
> +               /* compare names after chroot and working dir are applied
> */
> +               f = fname_after_chroot(s->str, cfg, 1);
> +               if(!f) fatal_exit("out of memory");
> +               if(strcmp(fname, f) == 0) {
> +                       free(f);
> +                       return 1;
> +               }
> +#ifdef HAVE_FNMATCH
> +               if(fnmatch(f, fname, 0) == 0) {
> +                       log_err("trusted-keys-file: \"%s\" matches
> zonefile '%s'",
> +                               s->str, fname);
> +                       free(f);
> +                       return 1;
> +               }
> +#endif
> +               free(f);
> +       }
> +       return 0;
> +}
> +
> +/** Check if the auth-zone/rpz zonefile: conflicts with other files,
> + * so it would overwrite that file. Refuse it aliasing any read-side
> bootstrap
> + * file. */
> +static void
> +check_file_clobber(struct config_file* cfg)
> +{
> +       struct config_auth* p;
> +       char* zfile, *sourceopt = NULL;
> +       for(p = cfg->auths; p; p = p->next) {
> +               if(!p->name || p->name[0] == 0)
> +                       continue; /* skip if no name */
> +               if(!p->zonefile || p->zonefile[0]==0)
> +                       continue; /* no zone file */
> +               zfile = fname_after_chroot(p->zonefile, cfg, 1);
> +               if(!zfile) fatal_exit("out of memory");
> +               if(file_list_contains(cfg->auto_trust_anchor_file_list,
> zfile,
> +                       cfg))
> +                       sourceopt = "auto-trust-anchor-file";
> +               else if(file_list_contains(cfg->trust_anchor_file_list,
> zfile,
> +                       cfg))
> +                       sourceopt = "trust-anchor-file";
> +               else
> if(file_list_contains_wild(cfg->trusted_keys_file_list,
> +                       zfile, cfg))
> +                       sourceopt = "trusted-keys-file";
> +               else if(file_list_contains(cfg->root_hints, zfile, cfg))
> +                       sourceopt = "root-hints";
> +               else
> if(file_list_contains(cfg->tls_session_ticket_keys.first,
> +                       zfile, cfg))
> +                       sourceopt = "tls-session-ticket-keys";
> +#ifdef USE_IPSECMOD
> +               if(cfg->ipsecmod_enabled &&
> +                       file_string_matches(cfg->ipsecmod_hook, zfile,
> cfg))
> +                       sourceopt = "ipsecmod-hook";
> +#endif
> +               if(sourceopt)
> +                       fatal_exit("auth-zone '%s': zonefile \"%s\" "
> +                               "is the same path as a %s option. "
> +                               "The auth-zone transfer would overwrite
> it.",
> +                               p->name, p->zonefile, sourceopt);
> +               free(zfile);
> +       }
> +}
> +
>  /** check configuration for errors */
>  static void
>  morechecks(struct config_file* cfg)
> @@ -822,6 +941,7 @@ morechecks(struct config_file* cfg)
>                         cfg->chrootdir, cfg);
>         }
>  #endif
> +       check_file_clobber(cfg);
>         /* remove chroot setting so that modules are not stripping
> pathnames */
>         free(cfg->chrootdir);
>         cfg->chrootdir = NULL;
> Index: smallapp/worker_cb.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/smallapp/worker_cb.c,v
> diff -u -p -r1.15 worker_cb.c
> --- smallapp/worker_cb.c        27 Jul 2026 14:14:39 -0000      1.15
> +++ smallapp/worker_cb.c        20 Sep 2026 09:50:48 -0000
> @@ -43,6 +43,7 @@
>  #include "config.h"
>  #include "libunbound/context.h"
>  #include "libunbound/worker.h"
> +#include "libunbound/remote.h"
>  #include "util/fptr_wlist.h"
>  #include "util/log.h"
>  #include "services/mesh.h"
> @@ -102,7 +103,7 @@ struct outbound_entry* worker_send_query
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream), int
> ATTR_UNUSED(ssl_upstream),
>         char* ATTR_UNUSED(tls_auth_name), struct module_qstate*
> ATTR_UNUSED(q),
> -       int* ATTR_UNUSED(was_ratelimited))
> +       int* ATTR_UNUSED(was_ratelimited), int*
> ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> @@ -142,7 +143,7 @@ struct outbound_entry* libworker_send_qu
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream), int
> ATTR_UNUSED(ssl_upstream),
>         char* ATTR_UNUSED(tls_auth_name), struct module_qstate*
> ATTR_UNUSED(q),
> -       int* ATTR_UNUSED(was_ratelimited))
> +       int* ATTR_UNUSED(was_ratelimited), int*
> ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> Index: testcode/dohclient.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/dohclient.c,v
> diff -u -p -r1.1.1.9 dohclient.c
> --- testcode/dohclient.c        26 May 2026 11:10:51 -0000      1.1.1.9
> +++ testcode/dohclient.c        20 Sep 2026 09:50:48 -0000
> @@ -146,7 +146,9 @@ submit_query(struct http2_session* h2_se
>  {
>         int32_t stream_id;
>         struct http2_stream* h2_stream;
> -       nghttp2_nv headers[5];
> +       nghttp2_nv headers[6];
> +       size_t num_headers = 5;
> +       char clen[16];
>         char* qb64;
>         size_t qb64_size;
>         size_t qb64_expected_size;
> @@ -194,9 +196,16 @@ submit_query(struct http2_session* h2_se
>         headers[3].value = (uint8_t*)h2_session->authority;
>         headers[4].name = (uint8_t*)"content-type";
>         headers[4].value = (uint8_t*)h2_session->content_type;
> +       if(h2_session->post) {
> +               snprintf(clen, sizeof(clen), "%u",
> +                       (unsigned)sldns_buffer_remaining(buf));
> +               headers[5].name = (uint8_t*)"content-length";
> +               headers[5].value = (uint8_t*)clen;
> +               num_headers = 6;
> +       }
>
>         printf("Request headers\n");
> -       for(i=0; i<sizeof(headers)/sizeof(headers[0]); i++) {
> +       for(i=0; i<num_headers; i++) {
>                 headers[i].namelen = strlen((char*)headers[i].name);
>                 headers[i].valuelen = strlen((char*)headers[i].value);
>                 headers[i].flags = NGHTTP2_NV_FLAG_NONE;
> @@ -204,7 +213,7 @@ submit_query(struct http2_session* h2_se
>         }
>
>         stream_id = nghttp2_submit_request(h2_session->session, NULL,
> headers,
> -               sizeof(headers)/sizeof(headers[0]),
> +               num_headers,
>                 (h2_session->post) ? &data_prd : NULL, h2_stream);
>         if(stream_id < 0) {
>                 printf("Failed to submit nghttp2 request");
> Index: testcode/doqclient.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/doqclient.c,v
> diff -u -p -r1.1.1.5 doqclient.c
> --- testcode/doqclient.c        27 Jul 2026 14:13:39 -0000      1.1.1.5
> +++ testcode/doqclient.c        20 Sep 2026 09:50:48 -0000
> @@ -1137,8 +1137,11 @@ static struct ngtcp2_conn* conn_client_s
>                 client_chosen_version, &cbs, &settings, &params,
>                 NULL, /* ngtcp2_mem allocator, use default */
>                 data /* callback argument */);
> -       if(!conn) fatal_exit("could not ngtcp2_conn_client_new: %s",
> -               ngtcp2_strerror(rv));
> +       if(rv!=0) {
> +               conn = NULL;
> +               fatal_exit("could not ngtcp2_conn_client_new: %s",
> +                       ngtcp2_strerror(rv));
> +       }
>         data->cc_algo = settings.cc_algo;
>         return conn;
>  }
> @@ -2098,7 +2101,7 @@ early_data_setup_session(struct doq_clie
>                 SSL_SESSION_free(session);
>                 return 0;
>         }
> -#ifdef USE_NGTCP2_CRYPTO_OSSL
> +#ifdef HAVE_SSL_SET_QUIC_TLS_EARLY_DATA_ENABLED
>         SSL_set_quic_tls_early_data_enabled(data->ssl, 1);
>  #else
>         SSL_set_quic_early_data_enabled(data->ssl, 1);
> @@ -2595,7 +2598,8 @@ struct outbound_entry* worker_send_query
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> @@ -2629,7 +2633,8 @@ struct outbound_entry* libworker_send_qu
>         socklen_t ATTR_UNUSED(addrlen), uint8_t* ATTR_UNUSED(zone),
>         size_t ATTR_UNUSED(zonelen), int ATTR_UNUSED(tcp_upstream),
>         int ATTR_UNUSED(ssl_upstream), char* ATTR_UNUSED(tls_auth_name),
> -       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited))
> +       struct module_qstate* ATTR_UNUSED(q), int*
> ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         log_assert(0);
>         return 0;
> Index: testcode/fake_event.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/fake_event.c,v
> diff -u -p -r1.1.1.20 fake_event.c
> --- testcode/fake_event.c       27 Jul 2026 14:13:40 -0000      1.1.1.20
> +++ testcode/fake_event.c       20 Sep 2026 09:50:48 -0000
> @@ -1276,7 +1276,8 @@ struct serviced_query* outnet_serviced_q
>         socklen_t addrlen, uint8_t* zone, size_t zonelen,
>         struct module_qstate* qstate, comm_point_callback_type* callback,
>         void* callback_arg, sldns_buffer* ATTR_UNUSED(buff),
> -       struct module_env* env, int* ATTR_UNUSED(was_ratelimited))
> +       struct module_env* env, int* ATTR_UNUSED(was_ratelimited),
> +       int* ATTR_UNUSED(ratelimit_incremented))
>  {
>         struct replay_runtime* runtime = (struct
> replay_runtime*)outnet->base;
>         struct fake_pending* pend = (struct fake_pending*)calloc(1,
> Index: testcode/testbound.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/testbound.c,v
> diff -u -p -r1.1.1.14 testbound.c
> --- testcode/testbound.c        26 Sep 2025 07:30:48 -0000      1.1.1.14
> +++ testcode/testbound.c        20 Sep 2026 09:50:48 -0000
> @@ -786,3 +786,13 @@ size_t doq_table_quic_size_get(struct do
>         return 0;
>  }
>  #endif
> +
> +void tcp_read_again_cb(void* ATTR_UNUSED(arg))
> +{
> +       /* nothing */
> +}
> +
> +void tcp_more_read_again_cb(void* ATTR_UNUSED(arg))
> +{
> +       /* nothing */
> +}
> Index: testcode/unitauth.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/unitauth.c,v
> diff -u -p -r1.1.1.7 unitauth.c
> --- testcode/unitauth.c 26 Sep 2025 07:30:47 -0000      1.1.1.7
> +++ testcode/unitauth.c 20 Sep 2026 09:50:48 -0000
> @@ -1027,6 +1027,38 @@ authzone_query_test(void)
>         check_queries("example.com", zone_example_com,
> example_com_queries);
>  }
>
> +/** Test chunkline_count_parens output */
> +static void
> +authzone_chunkline_count_parens_test(void)
> +{
> +       sldns_buffer* buf;
> +       if(vbmp) printf("Testing chunkline_count_parens\n");
> +       buf = sldns_buffer_new(1024);
> +       if(!buf) fatal_exit("out of memory");
> +
> +       /* Check that escaped characters are handled, '\x', and in quotes.
> */
> +       sldns_buffer_printf(buf, "TXT \"x\" \\(");
> +       unit_assert(chunkline_count_parens(buf, 0) == 0);
> +
> +       sldns_buffer_clear(buf);
> +       sldns_buffer_printf(buf, "TXT ';x' (");
> +       unit_assert(chunkline_count_parens(buf, 0) == 0);
> +
> +       sldns_buffer_clear(buf);
> +       sldns_buffer_printf(buf, "TXT \"a;b\" (");
> +       unit_assert(chunkline_count_parens(buf, 0) == 1);
> +
> +       sldns_buffer_clear(buf);
> +       sldns_buffer_printf(buf, "TXT \\) )");
> +       unit_assert(chunkline_count_parens(buf, 0) == -1);
> +
> +       sldns_buffer_clear(buf);
> +       sldns_buffer_printf(buf, "TXT \"a\\\\\" \"(\" ");
> +       unit_assert(chunkline_count_parens(buf, 0) == 0);
> +
> +       sldns_buffer_free(buf);
> +}
> +
>  /** test authzone code */
>  void
>  authzone_test(void)
> @@ -1036,4 +1068,5 @@ authzone_test(void)
>         authzone_compare_serial();
>         authzone_read_test();
>         authzone_query_test();
> +       authzone_chunkline_count_parens_test();
>  }
> Index: testcode/unitecs.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/unitecs.c,v
> diff -u -p -r1.1.1.3 unitecs.c
> --- testcode/unitecs.c  20 Oct 2022 08:25:17 -0000      1.1.1.3
> +++ testcode/unitecs.c  20 Sep 2026 09:50:48 -0000
> @@ -141,6 +141,7 @@ static addrlen_t randomkey(addrkey_t **k
>         int bits = rand() % maxlen;
>         int bytes = bits/8 + (bits%8>0); /*ceil*/
>         *k = (addrkey_t *) malloc(bytes * sizeof(addrkey_t));
> +       if(!*k) fatal_exit("out of memory");
>         for (byte = 0; byte < bytes; byte++) {
>                 (*k)[byte] = (addrkey_t)(rand() & 0xFF);
>         }
> Index: testcode/unitldns.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/unitldns.c,v
> diff -u -p -r1.1.1.7 unitldns.c
> --- testcode/unitldns.c 26 May 2026 11:10:51 -0000      1.1.1.7
> +++ testcode/unitldns.c 20 Sep 2026 09:50:48 -0000
> @@ -279,10 +279,24 @@ b64_test(void)
>         unit_assert(result == -1);
>  }
>
> +/** test SVCB ech svcparam */
> +static void
> +svcb_ech_test(void)
> +{
> +       uint8_t rr[LDNS_RR_BUF_SIZE];
> +       size_t rr_len = sizeof(rr), dname_len = 0;
> +       int e = sldns_str2wire_rr_buf("x. 300 IN HTTPS 1 . ech=0",
> +               rr, &rr_len, &dname_len, 300, NULL, 0, NULL, 0);
> +       unit_assert(e == LDNS_WIREPARSE_ERR_OK);
> +       unit_assert(rr_len == dname_len + 10 /* type,class,ttl,rdatalen */
> + 7 /* rdata */);
> +       unit_assert(sldns_read_uint16(rr + dname_len + 8 /* rdlen */) ==
> 7);
> +}
> +
>  void
>  ldns_test(void)
>  {
>         unit_show_feature("sldns");
>         rr_tests();
>         b64_test();
> +       svcb_ech_test();
>  }
> Index: testcode/unitmain.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/unitmain.c,v
> diff -u -p -r1.1.1.15 unitmain.c
> --- testcode/unitmain.c 27 Jul 2026 14:13:39 -0000      1.1.1.15
> +++ testcode/unitmain.c 20 Sep 2026 09:50:48 -0000
> @@ -1445,6 +1445,9 @@ main(int argc, char* argv[])
>  #  ifdef HAVE_RAND_CLEANUP
>         RAND_cleanup();
>  #  endif
> +#ifdef HAVE_OPENSSL_CLEANUP
> +       OPENSSL_cleanup();
> +#endif
>  #elif defined(HAVE_NSS)
>         if(NSS_Shutdown() != SECSuccess)
>                 fatal_exit("could not shutdown NSS");
> Index: testcode/unitverify.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/testcode/unitverify.c,v
> diff -u -p -r1.6 unitverify.c
> --- testcode/unitverify.c       26 May 2026 11:14:11 -0000      1.6
> +++ testcode/unitverify.c       20 Sep 2026 09:50:48 -0000
> @@ -196,7 +196,7 @@ verifytest_rrset(struct module_env* env,
>         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, &verified, reasonbuf,
> +               NULL, LDNS_SECTION_ANSWER, NULL, NULL, &verified,
> reasonbuf,
>                 sizeof(reasonbuf));
>         if(vsig) {
>                 printf("verify outcome is: %s %s\n",
> sec_status_to_string(sec),
> @@ -510,6 +510,146 @@ nsec3_hash_test(const char* fname)
>         sldns_buffer_free(buf);
>  }
>
> +/** Test the rrset_canonicalize_to_buffer function to see if the
> + * size of canon_owner name is properly checked for. */
> +static void
> +canon_owner_buf_test(void)
> +{
> +       struct regional* region;
> +       sldns_buffer* buf;
> +       struct ub_packed_rrset_key k;
> +       struct packed_rrset_data d;
> +       size_t rr_len[2];
> +       time_t rr_ttl[2];
> +       uint8_t* rr_data[2];
> +       int ret;
> +       unit_show_func("validator/val_sigcrypt.c",
> +               "rrset_canonicalize_to_buffer");
> +       region = regional_create();
> +       if(!region)
> +               fatal_exit("out of memory");
> +       /* Purposefully a very small buffer, to overflow it */
> +       buf = sldns_buffer_new(28);
> +       if(!buf)
> +               fatal_exit("out of memory");
> +
> +       /* An RRset to canonicalize. The buffer is made smaller, so
> +        * it can fail on bounds checks. */
> +       memset(&d, 0, sizeof(d));
> +       d.ttl = 3600;
> +       d.count = 1;
> +       d.rrsig_count = 1;
> +       d.rr_len = rr_len;
> +       d.rr_ttl = rr_ttl;
> +       d.rr_data = rr_data;
> +       rr_len[0] = 18;
> +       rr_len[1] = 36;
> +       rr_ttl[0] = 3600;
> +       rr_ttl[1] = 3600;
> +       rr_data[0] = (uint8_t*)"\x00\x10\x0Fzzaaaaaaaaaaaaa";
> +       rr_data[1] =
> (uint8_t*)"\x00\x24\x00\x06\x08\x3\x01\x02\x03\x04\x01\x02\x03\x04\x01\x02\x03\x04\x12\x34\x03zzz\x00zzaaaaaaaaaaa";
> +
> +       memset(&k, 0, sizeof(k));
> +       k.rk.dname = (uint8_t*) "\x0f" "aaaaaaaaaaaaaaa" "\x00";
> +       k.rk.dname_len = 17;
> +       k.rk.type = htons(LDNS_RR_TYPE_TXT);
> +       k.rk.rrset_class = htons(LDNS_RR_CLASS_IN);
> +       k.entry.data = &d;
> +
> +       /* There should be no buffer overflow, assertion failure, here */
> +       ret = rrset_canonicalize_to_buffer(region, buf, &k);
> +       unit_assert(ret == 0);
> +
> +       regional_destroy(region);
> +       sldns_buffer_free(buf);
> +}
> +
> +/** Test if ds_digest_match_dnskey that calls ds_create_dnskey_digest,
> + * checks the buffer size. */
> +static void
> +dnskey_ds_digest_test(void)
> +{
> +       struct regional* region;
> +       sldns_buffer* buf;
> +       struct module_env env;
> +       struct ub_packed_rrset_key k1, k2;
> +       struct packed_rrset_data d1, d2;
> +       size_t rr_len1[1], rr_len2[1];
> +       time_t rr_ttl1[1], rr_ttl2[1];
> +       uint8_t* rr_rdata1[1], *rr_rdata2[1];
> +       int ret;
> +       unit_show_func("validator/val_sigcrypt.c",
> "ds_digest_match_dnskey");
> +       region = regional_create();
> +       if(!region)
> +               fatal_exit("out of memory");
> +       /* Purposefully a very small buffer, to overflow it */
> +       buf = sldns_buffer_new(28);
> +       if(!buf)
> +               fatal_exit("out of memory");
> +       memset(&env, 0, sizeof(env));
> +       env.scratch = region;
> +       env.scratch_buffer = buf;
> +
> +       /* A DNSKEY and DS RRset to match together. The buffer is made
> +        * smaller, so it can fail on bounds checks. */
> +       memset(&d1, 0, sizeof(d1));
> +       d1.ttl = 3600;
> +       d1.count = 1;
> +       d1.rr_len = rr_len1;
> +       d1.rr_ttl = rr_ttl1;
> +       d1.rr_data = rr_rdata1;
> +       rr_len1[0] = 38;
> +       rr_ttl1[0] = 3600;
> +       /* DS rdata has: keytag (2bytes), algorithm (1byte),
> +        * digesttype (1byte), digest (remainder). */
> +       rr_rdata1[0] = (uint8_t*)"\x00\x24"
> +               "\x12\x34"
> +               "\x08" /* RSASHA256 */
> +               "\x02" /* SHA256 */
> +               "0123456789abcdef0123456789abcdef"; /* 32 bytes */
> +               ;
> +
> +       memset(&k1, 0, sizeof(k1));
> +       k1.rk.dname = (uint8_t*) "\x03" "foo" "\x00";
> +       k1.rk.dname_len = 5;
> +       k1.rk.type = htons(LDNS_RR_TYPE_DS);
> +       k1.rk.rrset_class = htons(LDNS_RR_CLASS_IN);
> +       k1.entry.data = &d1;
> +
> +       memset(&d2, 0, sizeof(d2));
> +       d2.ttl = 3600;
> +       d2.count = 1;
> +       d2.rr_len = rr_len2;
> +       d2.rr_ttl = rr_ttl2;
> +       d2.rr_data = rr_rdata2;
> +       rr_len2[0] = 38;
> +       rr_ttl2[0] = 3600;
> +       /* DNSKEY rdata has: flags (2bytes), protocol (1byte),
> +        * algorithm (1byte), publickey (remainder). */
> +       rr_rdata2[0] = (uint8_t*)"\x00\x24"
> +               "\x01\x01" /* KSK */
> +               "\x03" /* DNSSEC_KEYPROTO */
> +               "\x08" /* RSASHA256 */
> +               "0123456789abcdef0123456789abcdef"; /* 32 bytes of content
> */
> +               ;
> +
> +       memset(&k2, 0, sizeof(k2));
> +       k2.rk.dname = (uint8_t*) "\x03" "foo" "\x00";
> +       k2.rk.dname_len = 5;
> +       k2.rk.type = htons(LDNS_RR_TYPE_DNSKEY);
> +       k2.rk.rrset_class = htons(LDNS_RR_CLASS_IN);
> +       k2.entry.data = &d2;
> +       /* 36 byte rdata length for DNSKEY (38-2), and dname length of 5,
> +        * exceeds the (small) buffer size. */
> +
> +       /* There should be no buffer overflow, assertion failure, here */
> +       ret = ds_digest_match_dnskey(&env, &k2, 0, &k1, 0);
> +       unit_assert(ret == 0);
> +
> +       regional_destroy(region);
> +       sldns_buffer_free(buf);
> +}
> +
>  #define xstr(s) str(s)
>  #define str(s) #s
>
> @@ -724,4 +864,6 @@ verify_test(void)
>  #endif
>         nsectest();
>         nsec3_hash_test(SRCDIRSTR "/testdata/test_nsec3_hash.1");
> +       dnskey_ds_digest_test();
> +       canon_owner_buf_test();
>  }
> Index: util/config_file.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/config_file.c,v
> diff -u -p -r1.42 config_file.c
> --- util/config_file.c  26 May 2026 11:14:11 -0000      1.42
> +++ util/config_file.c  20 Sep 2026 09:50:48 -0000
> @@ -46,6 +46,7 @@
>  #ifdef HAVE_TIME_H
>  #include <time.h>
>  #endif
> +#include <limits.h>
>  #include "util/log.h"
>  #include "util/configyyrename.h"
>  #include "util/config_file.h"
> @@ -93,7 +94,7 @@ struct config_parser_state* cfg_parser =
>  static void init_outgoing_availports(int* array, int num);
>
>  /** init cookie with random data */
> -static void init_cookie_secret(uint8_t* cookie_secret, size_t
> cookie_secret_len);
> +static int init_cookie_secret(struct config_file* cfg);
>
>  struct config_file*
>  config_create(void)
> @@ -277,7 +278,7 @@ config_create(void)
>         cfg->val_sig_skew_min = 3600; /* at least daylight savings trouble
> */
>         cfg->val_sig_skew_max = 86400; /* at most timezone settings
> trouble */
>         cfg->val_max_restart = 5;
> -       cfg->val_clean_additional = 1;
> +       cfg->val_clean_additional = 0; /* off to protect against much
> data. */
>         cfg->val_log_level = 0;
>         cfg->val_log_squelch = 0;
>         cfg->val_permissive_mode = 0;
> @@ -389,8 +390,7 @@ config_create(void)
>  #endif
>         cfg->do_answer_cookie = 0;
>         memset(cfg->cookie_secret, 0, sizeof(cfg->cookie_secret));
> -       cfg->cookie_secret_len = 16;
> -       init_cookie_secret(cfg->cookie_secret, cfg->cookie_secret_len);
> +       cfg->cookie_secret_len = 0; /* not set yet */
>         cfg->cookie_secret_file = NULL;
>  #ifdef USE_CACHEDB
>         if(!(cfg->cachedb_backend = strdup("testframe"))) goto error_exit;
> @@ -429,6 +429,8 @@ config_create(void)
>         cfg->iter_scrub_rrsig = 8;
>         cfg->iter_scrub_promiscuous = 1;
>         cfg->max_global_quota = 200;
> +       cfg->val_validation_attempts = 32;
> +       cfg->val_hash_attempts = 32;
>         return cfg;
>  error_exit:
>         config_delete(cfg);
> @@ -533,7 +535,11 @@ probe_maxrto(int useful_server_top_timeo
>  int config_apply_max_rtt(int max_rtt)
>  {
>         USEFUL_SERVER_TOP_TIMEOUT = max_rtt;
> -       BLACKLIST_PENALTY = max_rtt*4;
> +       BLACKLIST_PENALTY =
> +#ifdef INT_MAX
> +               (max_rtt > INT_MAX/4) ? INT_MAX :
> +#endif
> +               max_rtt*4;
>         PROBE_MAXRTO = probe_maxrto(max_rtt);
>         return max_rtt;
>  }
> @@ -776,11 +782,13 @@ int config_set_option(struct config_file
>         else S_YNO("ede:", ede)
>         else S_YNO("ede-serve-expired:", ede_serve_expired)
>         else S_YNO("dns-error-reporting:", dns_error_reporting)
> -       else S_NUMBER_OR_ZERO("iter-scrub-ns:", iter_scrub_ns)
> +       else S_NUMBER_NONZERO("iter-scrub-ns:", iter_scrub_ns)
>         else S_NUMBER_OR_ZERO("iter-scrub-cname:", iter_scrub_cname)
>         else S_NUMBER_OR_ZERO("iter-scrub-rrsig:", iter_scrub_rrsig)
>         else S_YNO("iter-scrub-promiscuous:", iter_scrub_promiscuous)
>         else S_NUMBER_OR_ZERO("max-global-quota:", max_global_quota)
> +       else S_NUMBER_OR_ZERO("val-validation-attempts:",
> val_validation_attempts)
> +       else S_NUMBER_OR_ZERO("val-hash-attempts:", val_hash_attempts)
>         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)
> @@ -1261,6 +1269,8 @@ config_get_option(struct config_file* cf
>         else O_DEC(opt, "iter-scrub-rrsig", iter_scrub_rrsig)
>         else O_YNO(opt, "iter-scrub-promiscuous", iter_scrub_promiscuous)
>         else O_DEC(opt, "max-global-quota", max_global_quota)
> +       else O_DEC(opt, "val-validation-attempts", val_validation_attempts)
> +       else O_DEC(opt, "val-hash-attempts", val_hash_attempts)
>         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)
> @@ -1572,6 +1582,8 @@ config_read(struct config_file* cfg, con
>                 }
>                 globfree(&g);
>                 config_auto_slab_values(cfg);
> +               if(!init_cookie_secret(cfg))
> +                       return 0;
>                 return 1;
>         }
>  #endif /* HAVE_GLOB */
> @@ -1596,6 +1608,8 @@ config_read(struct config_file* cfg, con
>         }
>
>         config_auto_slab_values(cfg);
> +       if(!init_cookie_secret(cfg))
> +               return 0;
>         return 1;
>  }
>
> @@ -1870,18 +1884,33 @@ config_delete(struct config_file* cfg)
>         free(cfg);
>  }
>
> -static void
> -init_cookie_secret(uint8_t* cookie_secret, size_t cookie_secret_len)
> +static int
> +init_cookie_secret(struct config_file* cfg)
>  {
> -       struct ub_randstate *rand = ub_initstate(NULL);
> +       struct ub_randstate* rand;
> +       size_t cookie_secret_len;
> +       uint8_t* cookie_secret;
> +       if(!cfg->do_answer_cookie)
> +               return 1;
> +       if(cfg->cookie_secret_file && cfg->cookie_secret_file[0])
> +               return 1;
> +       if(cfg->cookie_secret_len != 0)
> +               return 1;
>
> -       if (!rand)
> -               fatal_exit("could not init random generator");
> +       rand = ub_initstate(NULL);
> +       if(!rand) {
> +               log_err("init_cookie_secret: could not init random
> generator");
> +               return 0;
> +       }
> +       cfg->cookie_secret_len = 16;
> +       cookie_secret_len = cfg->cookie_secret_len;
> +       cookie_secret = cfg->cookie_secret;
>         while (cookie_secret_len) {
>                 *cookie_secret++ = (uint8_t)ub_random(rand);
>                 cookie_secret_len--;
>         }
>         ub_randfree(rand);
> +       return 1;
>  }
>
>  static void
> @@ -1944,7 +1973,7 @@ extract_port_from_str(const char* str, i
>  int
>  cfg_mark_ports(const char* str, int allow, int* avail, int num)
>  {
> -       char* mid = strchr(str, '-');
> +       const char* mid = strchr(str, '-');
>  #ifdef DISABLE_EXPLICIT_PORT_RANDOMISATION
>         log_warn("Explicit port randomisation disabled, ignoring "
>                 "outgoing-port-permit and outgoing-port-avoid
> configuration "
> @@ -1952,7 +1981,7 @@ cfg_mark_ports(const char* str, int allo
>  #endif
>         if(!mid) {
>                 int port = extract_port_from_str(str, num);
> -               if(port < 0) {
> +               if (port < 0) {
>                         log_err("Failed to parse the port number");
>                         return 0;
>                 }
> @@ -1962,7 +1991,7 @@ cfg_mark_ports(const char* str, int allo
>                 char buf[16];
>                 int i, low;
>                 int high = extract_port_from_str(mid+1, num);
> -               if(high < 0) {
> +               if (high < 0) {
>                         log_err("Failed to parse the port number");
>                         return 0;
>                 }
> @@ -1976,7 +2005,7 @@ cfg_mark_ports(const char* str, int allo
>                         memcpy(buf, str, (size_t)(mid-str));
>                 buf[mid-str] = 0;
>                 low = extract_port_from_str(buf, num);
> -               if(low < 0) {
> +               if (low < 0) {
>                         log_err("Failed to parse the port number");
>                         return 0;
>                 }
> @@ -2647,10 +2676,10 @@ fname_after_chroot(const char* fname, st
>  }
>
>  /** return next space character in string */
> -static char* next_space_pos(const char* str)
> +static const char* next_space_pos(const char* str)
>  {
> -       char* sp = strchr(str, ' ');
> -       char* tab = strchr(str, '\t');
> +       const char* sp = strchr(str, ' ');
> +       const char* tab = strchr(str, '\t');
>         if(!tab && !sp)
>                 return NULL;
>         if(!sp) return tab;
> @@ -2659,10 +2688,10 @@ static char* next_space_pos(const char*
>  }
>
>  /** return last space character in string */
> -static char* last_space_pos(const char* str)
> +static const char* last_space_pos(const char* str)
>  {
> -       char* sp = strrchr(str, ' ');
> -       char* tab = strrchr(str, '\t');
> +       const char* sp = strrchr(str, ' ');
> +       const char* tab = strrchr(str, '\t');
>         if(!tab && !sp)
>                 return NULL;
>         if(!sp) return tab;
> @@ -2720,8 +2749,8 @@ cfg_parse_local_zone(struct config_file*
>
>  char* cfg_ptr_reverse(char* str)
>  {
> -       char* ip, *ip_end;
> -       char* name;
> +       const char* ip, *ip_end;
> +       const char* name;
>         char* result;
>         char buf[1024];
>         struct sockaddr_storage addr;
> @@ -2872,7 +2901,7 @@ if_listens_on(const char* ifname, int de
>         struct config_strlist* additional_ports)
>  {
>         struct config_strlist* s;
> -       char* p = strchr(ifname, '@');
> +       const char* p = strchr(ifname, '@');
>         int if_port;
>         if(p) if_port = atoi(p+1);
>         else  if_port = default_port;
> Index: util/config_file.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/config_file.h,v
> diff -u -p -r1.39 config_file.h
> --- util/config_file.h  26 May 2026 11:14:11 -0000      1.39
> +++ util/config_file.h  20 Sep 2026 09:50:48 -0000
> @@ -798,6 +798,10 @@ struct config_file {
>         int iter_scrub_rrsig;
>         /** limit on upstream queries for an incoming query and
> subqueries. */
>         int max_global_quota;
> +       /** limit on validator validation attempts. */
> +       int val_validation_attempts;
> +       /** limit on validator hash attempts. */
> +       int val_hash_attempts;
>         /** Should the iterator scrub promiscuous NS rrsets, from positive
>          * answers. */
>         int iter_scrub_promiscuous;
> @@ -884,6 +888,10 @@ struct config_auth {
>         int zonemd_check;
>         /** Reject absence of ZONEMD records, zone must have one */
>         int zonemd_reject_absence;
> +       /** The maximum auth zone transfer size, in bytes. */
> +       size_t max_transfer_size;
> +       /** The maximum auth zone transfer time taken, in msec. */
> +       int max_transfer_time;
>  };
>
>  /**
> Index: util/configlexer.lex
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/configlexer.lex,v
> diff -u -p -r1.35 configlexer.lex
> --- util/configlexer.lex        26 May 2026 11:14:11 -0000      1.35
> +++ util/configlexer.lex        20 Sep 2026 09:50:48 -0000
> @@ -608,6 +608,10 @@ iter-scrub-ns{COLON}               { YDVAR(1, VAR_ITE
>  iter-scrub-cname{COLON}                { YDVAR(1, VAR_ITER_SCRUB_CNAME) }
>  iter-scrub-rrsig{COLON}                { YDVAR(1, VAR_ITER_SCRUB_RRSIG) }
>  max-global-quota{COLON}                { YDVAR(1, VAR_MAX_GLOBAL_QUOTA) }
> +val-validation-attempts{COLON} { YDVAR(1, VAR_VAL_VALIDATION_ATTEMPTS) }
> +val-hash-attempts{COLON}       { YDVAR(1, VAR_VAL_HASH_ATTEMPTS) }
> +max-transfer-size{COLON}       { YDVAR(1, VAR_MAX_TRANSFER_SIZE) }
> +max-transfer-time{COLON}       { YDVAR(1, VAR_MAX_TRANSFER_TIME) }
>  iter-scrub-promiscuous{COLON}  { YDVAR(1, VAR_ITER_SCRUB_PROMISCUOUS) }
>  <INITIAL,val>{NEWLINE}         { LEXOUT(("NL\n")); cfg_parser->line++; }
>
> Index: util/configparser.y
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/configparser.y,v
> diff -u -p -r1.37 configparser.y
> --- util/configparser.y 26 May 2026 11:14:11 -0000      1.37
> +++ util/configparser.y 20 Sep 2026 09:50:48 -0000
> @@ -216,7 +216,9 @@ extern struct config_parser_state* cfg_p
>  %token VAR_LOG_DESTADDR VAR_CACHEDB_CHECK_WHEN_SERVE_EXPIRED
>  %token VAR_COOKIE_SECRET_FILE VAR_ITER_SCRUB_NS VAR_ITER_SCRUB_CNAME
>  %token VAR_ITER_SCRUB_RRSIG
> +%token VAR_MAX_TRANSFER_SIZE VAR_MAX_TRANSFER_TIME
>  %token VAR_MAX_GLOBAL_QUOTA VAR_HARDEN_UNVERIFIED_GLUE VAR_LOG_TIME_ISO
> +%token VAR_VAL_VALIDATION_ATTEMPTS VAR_VAL_HASH_ATTEMPTS
>  %token VAR_ITER_SCRUB_PROMISCUOUS VAR_LOG_THREAD_ID
>
>  %%
> @@ -359,7 +361,8 @@ content_server: server_num_threads | ser
>         server_harden_unknown_additional | server_disable_edns_do |
>         server_log_destaddr | server_cookie_secret_file |
>         server_iter_scrub_ns | server_iter_scrub_cname |
> server_max_global_quota |
> -       server_iter_scrub_rrsig |
> +       server_val_validation_attempts |
> +       server_val_hash_attempts | server_iter_scrub_rrsig |
>         server_harden_unverified_glue | server_log_time_iso |
> server_iter_scrub_promiscuous
>         ;
>  stub_clause: stubstart contents_stub
> @@ -459,6 +462,8 @@ authstart: VAR_AUTH_ZONE
>                         s->zonemd_check = 0;
>                         s->zonemd_reject_absence = 0;
>                         s->isrpz = 0;
> +                       s->max_transfer_size = 0;
> +                       s->max_transfer_time = 0;
>                 } else {
>                         yyerror("out of memory");
>                 }
> @@ -468,7 +473,8 @@ contents_auth: contents_auth content_aut
>         | ;
>  content_auth: auth_name | auth_zonefile | auth_master | auth_url |
>         auth_for_downstream | auth_for_upstream | auth_fallback_enabled |
> -       auth_allow_notify | auth_zonemd_check | auth_zonemd_reject_absence
> +       auth_allow_notify | auth_zonemd_check | auth_zonemd_reject_absence
> |
> +       auth_max_transfer_size | auth_max_transfer_time
>         ;
>
>  rpz_tag: VAR_TAGS STRING_ARG
> @@ -556,6 +562,8 @@ rpzstart: VAR_RPZ
>                         s->for_upstream = 0;
>                         s->fallback_enabled = 0;
>                         s->isrpz = 1;
> +                       s->max_transfer_size = 0;
> +                       s->max_transfer_time = 0;
>                 } else {
>                         yyerror("out of memory");
>                 }
> @@ -565,7 +573,8 @@ contents_rpz: contents_rpz content_rpz
>         | ;
>  content_rpz: auth_name | auth_zonefile | rpz_tag | auth_master | auth_url
> |
>            auth_allow_notify | rpz_action_override | rpz_cname_override |
> -          rpz_log | rpz_log_name | rpz_signal_nxdomain_ra |
> auth_for_downstream
> +          rpz_log | rpz_log_name | rpz_signal_nxdomain_ra |
> auth_for_downstream |
> +          auth_max_transfer_size | auth_max_transfer_time
>         ;
>  server_num_threads: VAR_NUM_THREADS STRING_ARG
>         {
> @@ -657,7 +666,7 @@ server_send_client_subnet: VAR_SEND_CLIE
>         #ifdef CLIENT_SUBNET
>                 OUTYY(("P(server_send_client_subnet:%s)\n", $2));
>                 if(!cfg_strlist_insert(&cfg_parser->cfg->client_subnet,
> $2))
> -                       fatal_exit("out of memory adding client-subnet");
> +                       yyerror("out of memory");
>         #else
>                 OUTYY(("P(Compiled without edns subnet option,
> ignoring)\n"));
>                 free($2);
> @@ -670,7 +679,7 @@ server_client_subnet_zone: VAR_CLIENT_SU
>                 OUTYY(("P(server_client_subnet_zone:%s)\n", $2));
>
> if(!cfg_strlist_insert(&cfg_parser->cfg->client_subnet_zone,
>                         $2))
> -                       fatal_exit("out of memory adding
> client-subnet-zone");
> +                       yyerror("out of memory");
>         #else
>                 OUTYY(("P(Compiled without edns subnet option,
> ignoring)\n"));
>                 free($2);
> @@ -2029,7 +2038,7 @@ server_access_control: VAR_ACCESS_CONTRO
>                 OUTYY(("P(server_access_control:%s %s)\n", $2, $3));
>                 validate_acl_action($3);
>                 if(!cfg_str2list_insert(&cfg_parser->cfg->acls, $2, $3))
> -                       fatal_exit("out of memory adding acl");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_interface_action: VAR_INTERFACE_ACTION STRING_ARG STRING_ARG
> @@ -2038,7 +2047,7 @@ server_interface_action: VAR_INTERFACE_A
>                 validate_acl_action($3);
>                 if(!cfg_str2list_insert(
>                         &cfg_parser->cfg->interface_actions, $2, $3))
> -                       fatal_exit("out of memory adding acl");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_module_conf: VAR_MODULE_CONF STRING_ARG
> @@ -2388,6 +2397,9 @@ server_local_zone: VAR_LOCAL_ZONE STRING
>                    && strcmp($3, "typetransparent")!=0
>                    && strcmp($3, "always_transparent")!=0
>                    && strcmp($3, "block_a")!=0
> +                  && strcmp($3, "block_aaaa")!=0
> +                  && strcmp($3, "block_a_wdata")!=0
> +                  && strcmp($3, "block_aaaa_wdata")!=0
>                    && strcmp($3, "always_refuse")!=0
>                    && strcmp($3, "always_nxdomain")!=0
>                    && strcmp($3, "always_nodata")!=0
> @@ -2400,7 +2412,9 @@ server_local_zone: VAR_LOCAL_ZONE STRING
>                         yyerror("local-zone type: expected static, deny, "
>                                 "refuse, redirect, transparent, "
>                                 "typetransparent, inform, inform_deny, "
> -                               "inform_redirect, always_transparent,
> block_a, "
> +                               "inform_redirect, always_transparent, "
> +                               "block_a, block_aaaa, "
> +                               "block_a_wdata, block_aaaa_wdata, "
>                                 "always_refuse, always_nxdomain, "
>                                 "always_nodata, always_deny, always_null, "
>                                 "noview, nodefault or ipset");
> @@ -2409,7 +2423,7 @@ server_local_zone: VAR_LOCAL_ZONE STRING
>                 } else if(strcmp($3, "nodefault")==0) {
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->
>                                 local_zones_nodefault, $2))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                         free($3);
>  #ifdef USE_IPSET
>                 } else if(strcmp($3, "ipset")==0) {
> @@ -2417,21 +2431,24 @@ server_local_zone: VAR_LOCAL_ZONE STRING
>                         /* Make sure to add the trailing dot.
>                          * These are str compared to domain names. */
>                         if($2[len-1] != '.') {
> +                               char* prev = $2;
>                                 if(!($2 = realloc($2, len+2))) {
> -                                       fatal_exit("out of memory adding
> local-zone");
> +                                       yyerror("out of memory");
> +                                       free(prev);
> +                               } else {
> +                                       $2[len] = '.';
> +                                       $2[len+1] = 0;
>                                 }
> -                               $2[len] = '.';
> -                               $2[len+1] = 0;
>                         }
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->
>                                 local_zones_ipset, $2))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                         free($3);
>  #endif
>                 } else {
>
> if(!cfg_str2list_insert(&cfg_parser->cfg->local_zones,
>                                 $2, $3))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -2439,7 +2456,7 @@ server_local_data: VAR_LOCAL_DATA STRING
>         {
>                 OUTYY(("P(server_local_data:%s)\n", $2));
>                 if(!cfg_strlist_insert(&cfg_parser->cfg->local_data, $2))
> -                       fatal_exit("out of memory adding local-data");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_local_data_ptr: VAR_LOCAL_DATA_PTR STRING_ARG
> @@ -2451,7 +2468,7 @@ server_local_data_ptr: VAR_LOCAL_DATA_PT
>                 if(ptr) {
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->
>                                 local_data, ptr))
> -                               fatal_exit("out of memory adding
> local-data");
> +                               yyerror("out of memory");
>                 } else {
>                         yyerror("local-data-ptr could not be reversed");
>                 }
> @@ -2515,8 +2532,7 @@ server_wait_limit_netblock: VAR_WAIT_LIM
>                 } else {
>                         if(!cfg_str2list_insert(&cfg_parser->cfg->
>                                 wait_limit_netblock, $2, $3))
> -                               fatal_exit("out of memory adding "
> -                                       "wait-limit-netblock");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -2530,8 +2546,7 @@ server_wait_limit_cookie_netblock: VAR_W
>                 } else {
>                         if(!cfg_str2list_insert(&cfg_parser->cfg->
>                                 wait_limit_cookie_netblock, $2, $3))
> -                               fatal_exit("out of memory adding "
> -                                       "wait-limit-cookie-netblock");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -2563,7 +2578,7 @@ server_dns64_ignore_aaaa: VAR_DNS64_IGNO
>                 OUTYY(("P(dns64_ignore_aaaa:%s)\n", $2));
>                 if(!cfg_strlist_insert(&cfg_parser->cfg->dns64_ignore_aaaa,
>                         $2))
> -                       fatal_exit("out of memory adding
> dns64-ignore-aaaa");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_nat64_prefix: VAR_NAT64_PREFIX STRING_ARG
> @@ -2828,8 +2843,7 @@ server_ratelimit_for_domain: VAR_RATELIM
>                 } else {
>                         if(!cfg_str2list_insert(&cfg_parser->cfg->
>                                 ratelimit_for_domain, $2, $3))
> -                               fatal_exit("out of memory adding "
> -                                       "ratelimit-for-domain");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -2843,8 +2857,7 @@ server_ratelimit_below_domain: VAR_RATEL
>                 } else {
>                         if(!cfg_str2list_insert(&cfg_parser->cfg->
>                                 ratelimit_below_domain, $2, $3))
> -                               fatal_exit("out of memory adding "
> -                                       "ratelimit-below-domain");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -3078,8 +3091,7 @@ server_edns_client_string: VAR_EDNS_CLIE
>                 OUTYY(("P(server_edns_client_string:%s %s)\n", $2, $3));
>                 if(!cfg_str2list_insert(
>                         &cfg_parser->cfg->edns_client_strings, $2, $3))
> -                       fatal_exit("out of memory adding "
> -                               "edns-client-string");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_edns_client_string_opcode: VAR_EDNS_CLIENT_STRING_OPCODE STRING_ARG
> @@ -3341,6 +3353,23 @@ auth_fallback_enabled: VAR_FALLBACK_ENAB
>                 free($2);
>         }
>         ;
> +auth_max_transfer_size: VAR_MAX_TRANSFER_SIZE STRING_ARG
> +       {
> +               OUTYY(("P(max-transfer-size:%s)\n", $2));
> +               if(!cfg_parse_memsize($2,
> &cfg_parser->cfg->auths->max_transfer_size))
> +                       yyerror("memory size expected");
> +               free($2);
> +       }
> +       ;
> +auth_max_transfer_time: VAR_MAX_TRANSFER_TIME STRING_ARG
> +       {
> +               OUTYY(("P(max-transfer-time:%s)\n", $2));
> +               if(atoi($2) == 0 && strcmp($2, "0") != 0)
> +                       yyerror("number expected");
> +               else cfg_parser->cfg->auths->max_transfer_time = atoi($2);
> +               free($2);
> +       }
> +       ;
>  view_name: VAR_NAME STRING_ARG
>         {
>                 OUTYY(("P(name:%s)\n", $2));
> @@ -3380,7 +3409,7 @@ view_local_zone: VAR_LOCAL_ZONE STRING_A
>                 } else if(strcmp($3, "nodefault")==0) {
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->views->
>                                 local_zones_nodefault, $2))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                         free($3);
>  #ifdef USE_IPSET
>                 } else if(strcmp($3, "ipset")==0) {
> @@ -3388,22 +3417,25 @@ view_local_zone: VAR_LOCAL_ZONE STRING_A
>                         /* Make sure to add the trailing dot.
>                          * These are str compared to domain names. */
>                         if($2[len-1] != '.') {
> +                               char* prev = $2;
>                                 if(!($2 = realloc($2, len+2))) {
> -                                       fatal_exit("out of memory adding
> local-zone");
> +                                       yyerror("out of memory");
> +                                       free(prev);
> +                               } else {
> +                                       $2[len] = '.';
> +                                       $2[len+1] = 0;
>                                 }
> -                               $2[len] = '.';
> -                               $2[len+1] = 0;
>                         }
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->views->
>                                 local_zones_ipset, $2))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                         free($3);
>  #endif
>                 } else {
>                         if(!cfg_str2list_insert(
>                                 &cfg_parser->cfg->views->local_zones,
>                                 $2, $3))
> -                               fatal_exit("out of memory adding
> local-zone");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -3413,8 +3445,7 @@ view_response_ip: VAR_RESPONSE_IP STRING
>                 validate_respip_action($3);
>                 if(!cfg_str2list_insert(
>                         &cfg_parser->cfg->views->respip_actions, $2, $3))
> -                       fatal_exit("out of memory adding per-view "
> -                               "response-ip action");
> +                       yyerror("out of memory");
>         }
>         ;
>  view_response_ip_data: VAR_RESPONSE_IP_DATA STRING_ARG STRING_ARG
> @@ -3422,14 +3453,14 @@ view_response_ip_data: VAR_RESPONSE_IP_D
>                 OUTYY(("P(view_response_ip_data:%s)\n", $2));
>                 if(!cfg_str2list_insert(
>                         &cfg_parser->cfg->views->respip_data, $2, $3))
> -                       fatal_exit("out of memory adding
> response-ip-data");
> +                       yyerror("out of memory");
>         }
>         ;
>  view_local_data: VAR_LOCAL_DATA STRING_ARG
>         {
>                 OUTYY(("P(view_local_data:%s)\n", $2));
>
> if(!cfg_strlist_insert(&cfg_parser->cfg->views->local_data, $2)) {
> -                       fatal_exit("out of memory adding local-data");
> +                       yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -3442,7 +3473,7 @@ view_local_data_ptr: VAR_LOCAL_DATA_PTR
>                 if(ptr) {
>                         if(!cfg_strlist_insert(&cfg_parser->cfg->views->
>                                 local_data, ptr))
> -                               fatal_exit("out of memory adding
> local-data");
> +                               yyerror("out of memory");
>                 } else {
>                         yyerror("local-data-ptr could not be reversed");
>                 }
> @@ -3782,7 +3813,7 @@ server_response_ip: VAR_RESPONSE_IP STRI
>                 validate_respip_action($3);
>                 if(!cfg_str2list_insert(&cfg_parser->cfg->respip_actions,
>                         $2, $3))
> -                       fatal_exit("out of memory adding response-ip");
> +                       yyerror("out of memory");
>         }
>         ;
>  server_response_ip_data: VAR_RESPONSE_IP_DATA STRING_ARG STRING_ARG
> @@ -3790,7 +3821,7 @@ server_response_ip_data: VAR_RESPONSE_IP
>                 OUTYY(("P(server_response_ip_data:%s)\n", $2));
>                 if(!cfg_str2list_insert(&cfg_parser->cfg->respip_data,
>                         $2, $3))
> -                       fatal_exit("out of memory adding
> response-ip-data");
> +                       yyerror("out of memory");
>         }
>         ;
>  dnscstart: VAR_DNSCRYPT
> @@ -3838,26 +3869,30 @@ dnsc_dnscrypt_provider: VAR_DNSCRYPT_PRO
>  dnsc_dnscrypt_provider_cert: VAR_DNSCRYPT_PROVIDER_CERT STRING_ARG
>         {
>                 OUTYY(("P(dnsc_dnscrypt_provider_cert:%s)\n", $2));
> -
>  if(cfg_strlist_find(cfg_parser->cfg->dnscrypt_provider_cert, $2))
> +
>  if(cfg_strlist_find(cfg_parser->cfg->dnscrypt_provider_cert, $2)) {
>                         log_warn("dnscrypt-provider-cert %s is a
> duplicate", $2);
> -
>  if(!cfg_strlist_insert(&cfg_parser->cfg->dnscrypt_provider_cert, $2))
> -                       fatal_exit("out of memory adding
> dnscrypt-provider-cert");
> +                       free($2);
> +               } else
> if(!cfg_strlist_insert(&cfg_parser->cfg->dnscrypt_provider_cert, $2)) {
> +                       yyerror("out of memory");
> +               }
>         }
>         ;
>  dnsc_dnscrypt_provider_cert_rotated: VAR_DNSCRYPT_PROVIDER_CERT_ROTATED
> STRING_ARG
>         {
>                 OUTYY(("P(dnsc_dnscrypt_provider_cert_rotated:%s)\n", $2));
>
> if(!cfg_strlist_insert(&cfg_parser->cfg->dnscrypt_provider_cert_rotated,
> $2))
> -                       fatal_exit("out of memory adding
> dnscrypt-provider-cert-rotated");
> +                       yyerror("out of memory");
>         }
>         ;
>  dnsc_dnscrypt_secret_key: VAR_DNSCRYPT_SECRET_KEY STRING_ARG
>         {
>                 OUTYY(("P(dnsc_dnscrypt_secret_key:%s)\n", $2));
> -               if(cfg_strlist_find(cfg_parser->cfg->dnscrypt_secret_key,
> $2))
> +               if(cfg_strlist_find(cfg_parser->cfg->dnscrypt_secret_key,
> $2)) {
>                         log_warn("dnscrypt-secret-key: %s is a duplicate",
> $2);
> -
>  if(!cfg_strlist_insert(&cfg_parser->cfg->dnscrypt_secret_key, $2))
> -                       fatal_exit("out of memory adding
> dnscrypt-secret-key");
> +                       free($2);
> +               } else
> if(!cfg_strlist_insert(&cfg_parser->cfg->dnscrypt_secret_key, $2)) {
> +                       yyerror("out of memory");
> +               }
>         }
>         ;
>  dnsc_dnscrypt_shared_secret_cache_size:
> VAR_DNSCRYPT_SHARED_SECRET_CACHE_SIZE STRING_ARG
> @@ -4202,7 +4237,7 @@ server_tcp_connection_limit: VAR_TCP_CON
>                         yyerror("positive number expected");
>                 else {
>
> if(!cfg_str2list_insert(&cfg_parser->cfg->tcp_connection_limits, $2, $3))
> -                               fatal_exit("out of memory adding tcp
> connection limit");
> +                               yyerror("out of memory");
>                 }
>         }
>         ;
> @@ -4241,8 +4276,8 @@ server_cookie_secret_file: VAR_COOKIE_SE
>  server_iter_scrub_ns: VAR_ITER_SCRUB_NS STRING_ARG
>         {
>                 OUTYY(("P(server_iter_scrub_ns:%s)\n", $2));
> -               if(atoi($2) == 0 && strcmp($2, "0") != 0)
> -                       yyerror("number expected");
> +               if(atoi($2) < 1)
> +                       yyerror("number >= 1 expected");
>                 else cfg_parser->cfg->iter_scrub_ns = atoi($2);
>                 free($2);
>         }
> @@ -4281,6 +4316,24 @@ server_iter_scrub_promiscuous: VAR_ITER_
>                         yyerror("expected yes or no.");
>                 else cfg_parser->cfg->iter_scrub_promiscuous =
>                         (strcmp($2, "yes")==0);
> +               free($2);
> +       }
> +       ;
> +server_val_validation_attempts: VAR_VAL_VALIDATION_ATTEMPTS STRING_ARG
> +       {
> +               OUTYY(("P(server_val_validation_attempts:%s)\n", $2));
> +               if(atoi($2) == 0 && strcmp($2, "0") != 0)
> +                       yyerror("number expected");
> +               else cfg_parser->cfg->val_validation_attempts = atoi($2);
> +               free($2);
> +       }
> +       ;
> +server_val_hash_attempts: VAR_VAL_HASH_ATTEMPTS STRING_ARG
> +       {
> +               OUTYY(("P(server_val_hash_attempts:%s)\n", $2));
> +               if(atoi($2) == 0 && strcmp($2, "0") != 0)
> +                       yyerror("number expected");
> +               else cfg_parser->cfg->val_hash_attempts = atoi($2);
>                 free($2);
>         }
>         ;
> Index: util/fptr_wlist.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/fptr_wlist.c,v
> diff -u -p -r1.31 fptr_wlist.c
> --- util/fptr_wlist.c   27 Jul 2026 14:14:39 -0000      1.31
> +++ util/fptr_wlist.c   20 Sep 2026 09:50:48 -0000
> @@ -141,6 +141,8 @@ fptr_whitelist_comm_timer(void (*fptr)(v
>  #ifdef UB_ON_WINDOWS
>         else if(fptr == &wsvc_cron_cb) return 1;
>  #endif
> +       else if(fptr == &tcp_read_again_cb) return 1;
> +       else if(fptr == &tcp_more_read_again_cb) return 1;
>         else if(fptr == &auth_xfer_timer) return 1;
>         else if(fptr == &auth_xfer_probe_timer_callback) return 1;
>         else if(fptr == &auth_xfer_transfer_timer_callback) return 1;
> @@ -362,7 +364,7 @@ fptr_whitelist_modenv_send_query(struct
>         int nocaps, int check_ratelimit, struct sockaddr_storage* addr,
>         socklen_t addrlen, uint8_t* zone, size_t zonelen, int tcp_upstream,
>         int ssl_upstream, char* tls_auth_name, struct module_qstate* q,
> -       int* was_ratelimited))
> +       int* was_ratelimited, int* ratelimit_incremented))
>  {
>         if(fptr == &worker_send_query) return 1;
>         else if(fptr == &libworker_send_query) return 1;
> @@ -413,7 +415,7 @@ fptr_whitelist_modenv_detect_cycle(int (
>         return 0;
>  }
>
> -int
> +int
>  fptr_whitelist_mod_init(int (*fptr)(struct module_env* env, int id))
>  {
>         if(fptr == &iter_init) return 1;
> @@ -441,7 +443,7 @@ fptr_whitelist_mod_init(int (*fptr)(stru
>         return 0;
>  }
>
> -int
> +int
>  fptr_whitelist_mod_deinit(void (*fptr)(struct module_env* env, int id))
>  {
>         if(fptr == &iter_deinit) return 1;
> Index: util/fptr_wlist.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/fptr_wlist.h,v
> diff -u -p -r1.13 fptr_wlist.h
> --- util/fptr_wlist.h   26 May 2026 11:14:11 -0000      1.13
> +++ util/fptr_wlist.h   20 Sep 2026 09:50:48 -0000
> @@ -214,7 +214,7 @@ int fptr_whitelist_modenv_send_query(str
>         int nocaps, int check_ratelimit, struct sockaddr_storage* addr,
>         socklen_t addrlen, uint8_t* zone, size_t zonelen, int tcp_upstream,
>         int ssl_upstream, char* tls_auth_name, struct module_qstate* q,
> -       int* was_ratelimited));
> +       int* was_ratelimited, int* ratelimit_incremented));
>
>  /**
>   * Check function pointer whitelist for module_env detach_subs callback
> values.
> Index: util/iana_ports.inc
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/iana_ports.inc,v
> diff -u -p -r1.31 iana_ports.inc
> --- util/iana_ports.inc 26 May 2026 11:14:11 -0000      1.31
> +++ util/iana_ports.inc 20 Sep 2026 09:50:48 -0000
> @@ -3866,6 +3866,7 @@
>  4456,
>  4457,
>  4458,
> +4480,
>  4484,
>  4486,
>  4488,
> @@ -4506,6 +4507,7 @@
>  6581,
>  6582,
>  6583,
> +6610,
>  6619,
>  6620,
>  6621,
> @@ -4608,6 +4610,7 @@
>  7101,
>  7107,
>  7121,
> +7123,
>  7128,
>  7129,
>  7161,
> @@ -5393,6 +5396,7 @@
>  30004,
>  30260,
>  30832,
> +30939,
>  30999,
>  31016,
>  31029,
> Index: util/module.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/module.h,v
> diff -u -p -r1.23 module.h
> --- util/module.h       27 Jul 2026 14:14:39 -0000      1.23
> +++ util/module.h       20 Sep 2026 09:50:48 -0000
> @@ -375,6 +375,8 @@ struct module_env {
>          * @param q: which query state to reactivate upon return.
>          * @param was_ratelimited: it will signal back if the query failed
> to pass the
>          *      ratelimit check.
> +        * @param ratelimit_incremented: set to true if the ratelimit
> counter
> +        *      was increased.
>          * @return: false on failure (memory or socket related). no query
> was
>          *      sent. Or returns an outbound entry with qsent and qstate
> set.
>          *      This outbound_entry will be used on later module
> invocations
> @@ -385,7 +387,8 @@ struct module_env {
>                 int check_ratelimit,
>                 struct sockaddr_storage* addr, socklen_t addrlen,
>                 uint8_t* zone, size_t zonelen, int tcp_upstream, int
> ssl_upstream,
> -               char* tls_auth_name, struct module_qstate* q, int*
> was_ratelimited);
> +               char* tls_auth_name, struct module_qstate* q, int*
> was_ratelimited,
> +               int* ratelimit_incremented);
>
>         /**
>          * Detach-subqueries.
> @@ -698,10 +701,16 @@ struct module_qstate {
>         time_t qstarttime;
>         /** whether a message from cachedb will be used for the reply */
>         int is_cachedb_answer;
> +       /** whether the reply is subnet specific */
> +       int is_subnet_answer;
>         /** if the response as error is from error_response_cache, and is
>          * suitable for caching (briefly) the error response. Set by the
>          * iterator when no_cache_store is enabled, and there is an error.
> */
>         int error_response_cache;
> +       /** if the iterator sees that the forward/stub has no_cache set.
> +        * to signal to calling modules that their setting of no_cache for
> +        * other reasons, has to take into account the fwd/stub no_cache.
> */
> +       int fwd_stub_no_cache;
>
>         /**
>          * Attributes of clients that share the qstate that may affect
> IP-based
> @@ -736,7 +745,7 @@ struct module_func_block {
>         /** text string name of module */
>         const char* name;
>
> -       /**
> +       /**
>          * Set up the module for start. This is called only once at
> startup.
>          * Privileged operations like opening device files may be done
> here.
>          * The function ptr can be NULL, if it is not used.
> Index: util/net_help.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/net_help.c,v
> diff -u -p -r1.36 net_help.c
> --- util/net_help.c     26 May 2026 11:14:11 -0000      1.36
> +++ util/net_help.c     20 Sep 2026 09:50:48 -0000
> @@ -242,7 +242,7 @@ int
>  extstrtoaddr(const char* str, struct sockaddr_storage* addr,
>         socklen_t* addrlen, int port)
>  {
> -       char* s;
> +       const char* s;
>         if((s=strchr(str, '@'))) {
>                 char buf[MAX_ADDR_STRLEN];
>                 if(s-str >= MAX_ADDR_STRLEN) {
> @@ -268,7 +268,7 @@ ipstrtoaddr(const char* ip, int port, st
>         p = (uint16_t) port;
>         if(str_is_ip6(ip)) {
>                 char buf[MAX_ADDR_STRLEN];
> -               char* s;
> +               const char* s;
>                 struct sockaddr_in6* sa = (struct sockaddr_in6*)addr;
>                 *addrlen = (socklen_t)sizeof(struct sockaddr_in6);
>                 memset(sa, 0, *addrlen);
> @@ -304,8 +304,9 @@ ipstrtoaddr(const char* ip, int port, st
>  int netblockstrtoaddr(const char* str, int port, struct sockaddr_storage*
> addr,
>          socklen_t* addrlen, int* net)
>  {
> +       const char* s;
>         char buf[64];
> -       char* s;
> +       char* b = NULL;
>         *net = (str_is_ip6(str)?128:32);
>         if((s=strchr(str, '/'))) {
>                 if(atoi(s+1) > *net) {
> @@ -323,15 +324,15 @@ int netblockstrtoaddr(const char* str, i
>                         return 0;
>                 }
>                 strlcpy(buf, str, sizeof(buf));
> -               s = strchr(buf, '/');
> -               if(s) *s = 0;
> -               s = buf;
> +               b = strchr(buf, '/');
> +               if(b) *b = 0;
> +               b = buf;
>         }
> -       if(!ipstrtoaddr(s?s:str, port, addr, addrlen)) {
> +       if(!ipstrtoaddr(b?b:str, port, addr, addrlen)) {
>                 log_err("cannot parse ip address: '%s'", str);
>                 return 0;
>         }
> -       if(s) {
> +       if(b) {
>                 addr_mask(addr, *addrlen, *net);
>         }
>         return 1;
> @@ -1445,6 +1446,8 @@ void* listen_sslctx_create(const char* k
>                 SSL_CTX_set_alpn_select_cb(ctx, doh_alpn_select_cb, NULL);
>  #endif
>         }
> +#else /* HAVE_SSL_CTX_SET_ALPN_SELECT_CB */
> +       (void)is_dot; (void)is_doh;
>  #endif /* HAVE_SSL_CTX_SET_ALPN_SELECT_CB */
>         return ctx;
>  #else
> @@ -1704,6 +1707,10 @@ int check_auth_name_for_ssl(char* auth_n
>  /** set the authname on an SSL structure, SSL* ssl */
>  int set_auth_name_on_ssl(void* ssl, char* auth_name, int use_sni)
>  {
> +#ifdef HAVE_SSL_SET1_DNSNAME
> +       struct sockaddr_storage tmpaddr;
> +       socklen_t tmpaddrlen = (socklen_t)sizeof(tmpaddr);
> +#endif
>         if(!auth_name) return 1;
>  #ifdef HAVE_SSL
>         if(use_sni) {
> @@ -1713,7 +1720,20 @@ int set_auth_name_on_ssl(void* ssl, char
>         (void)ssl;
>         (void)use_sni;
>  #endif
> -#ifdef HAVE_SSL_SET1_HOST
> +#ifdef HAVE_SSL_SET1_DNSNAME
> +       SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL);
> +       if(ipstrtoaddr(auth_name, UNBOUND_DNS_PORT, &tmpaddr,
> &tmpaddrlen)) {
> +               if(!SSL_set1_ipaddr(ssl, auth_name)) {
> +                       log_err("SSL_set1_ipaddr failed");
> +                       return 0;
> +               }
> +       } else {
> +               if(!SSL_set1_dnsname(ssl, auth_name)) {
> +                       log_err("SSL_set1_dnsname failed");
> +                       return 0;
> +               }
> +       }
> +#elif defined(HAVE_SSL_SET1_HOST)
>         SSL_set_verify(ssl, SSL_VERIFY_PEER, NULL);
>         /* setting the hostname makes openssl verify the
>          * host name in the x509 certificate in the
> Index: util/netevent.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/netevent.c,v
> diff -u -p -r1.44 netevent.c
> --- util/netevent.c     27 Jul 2026 14:14:39 -0000      1.44
> +++ util/netevent.c     20 Sep 2026 09:50:48 -0000
> @@ -122,6 +122,10 @@
>  #define NUM_UDP_PER_SELECT 1
>  #endif
>
> +/** The number of TCP queries over a TCP connection, per read indication
> + * from select. */
> +#define NUM_TCP_PER_SELECT 100
> +
>  /** timeout in millisec to wait for write to unblock, packets dropped
> after.*/
>  #define SEND_BLOCKED_WAIT_TIMEOUT 200
>  /** max number of times to wait for write to unblock, packets dropped
> after.*/
> @@ -951,6 +955,10 @@ static int consume_pp2_header(struct sld
>                         {
>                         struct sockaddr_in* addr =
>                                 (struct sockaddr_in*)&rep->client_addr;
> +                       if(ntohs(header->len) < PP2_HEADER_LEN_INET) {
> +                               verbose(VERB_OPS, "proxy_protocol: header
> too short for IPv4 address");
> +                               return 0;
> +                       }
>                         addr->sin_family = AF_INET;
>                         addr->sin_addr.s_addr =
> header->addr.addr4.src_addr;
>                         addr->sin_port = header->addr.addr4.src_port;
> @@ -963,6 +971,10 @@ static int consume_pp2_header(struct sld
>                         {
>                         struct sockaddr_in6* addr =
>                                 (struct sockaddr_in6*)&rep->client_addr;
> +                       if(ntohs(header->len) < PP2_HEADER_LEN_INET6) {
> +                               verbose(VERB_OPS, "proxy_protocol: header
> too short for IPv6 address");
> +                               return 0;
> +                       }
>                         memset(addr, 0, sizeof(*addr));
>                         addr->sin6_family = AF_INET6;
>                         memcpy(&addr->sin6_addr,
> @@ -2932,6 +2944,8 @@ setup_tcp_handler(struct comm_point* c,
>         c->tcp_is_reading = 1;
>         c->tcp_byte_count = 0;
>         c->tcp_keepalive = 0;
> +       /* reset to configured value before applying load-based reduction
> */
> +       c->tcp_timeout_msec = c->tcp_parent->tcp_timeout_msec;
>         /* if more than half the tcp handlers are in use, use a shorter
>          * timeout for this TCP connection, we need to make space for
>          * other connections to be able to get attention */
> @@ -2967,6 +2981,62 @@ void comm_base_handle_slow_accept(int AT
>         }
>  }
>
> +/** out of resources in the accept path: pause all listening for
> + * NETEVENT_SLOW_ACCEPT_TIME and re-arm via comm_base_handle_slow_accept.
> + *
> + * If the routine fails, the socket is accepted and then closed, draining
> it
> + * from the waiting list of connections to be accepted.
> + * @param c: the comm point that is a listening socket.
> + * @param msec: if 0: uses the slow accept time. Otherwise, sets the time
> + *             to wait.
> + */
> +static void
> +comm_point_slow_accept(struct comm_point* c, int msec)
> +{
> +       struct comm_base* b = c->ev->base;
> +       struct timeval tv;
> +       struct ub_event* slowev;
> +       if(!b->stop_accept)
> +               return;
> +       if(b->eb->slow_accept_enabled)
> +               return;
> +       /* Allocate the event */
> +       slowev = ub_event_new(b->eb->base, -1, UB_EV_TIMEOUT,
> +               comm_base_handle_slow_accept, b);
> +       if(!slowev) {
> +               /* The slow accept was not enabled yet, to handle
> +                * the allocation failure, instead drain the incoming
> +                * connection. */
> +               int new_fd = accept(c->fd, NULL, NULL);
> +               if(new_fd != -1) {
> +                       verbose(VERB_ALGO, "slow accept: event_new failed,
> "
> +                               "drop connection");
> +                       sock_close(new_fd);
> +               }
> +               return;
> +       }
> +       ub_comm_base_now(b);
> +       if(b->eb->last_slow_log+SLOW_LOG_TIME <= b->eb->secs) {
> +               b->eb->last_slow_log = b->eb->secs;
> +               verbose(VERB_OPS, "out of resources on accept, "
> +                       "slow down accept for %d msec",
> +                       NETEVENT_SLOW_ACCEPT_TIME);
> +       }
> +       b->eb->slow_accept_enabled = 1;
> +       fptr_ok(fptr_whitelist_stop_accept(b->stop_accept));
> +       (*b->stop_accept)(b->cb_arg);
> +       /* set timeout, no mallocs */
> +       if(msec == 0)
> +               msec = NETEVENT_SLOW_ACCEPT_TIME;
> +       tv.tv_sec = msec/1000;
> +       tv.tv_usec = (msec%1000)*1000;
> +       b->eb->slow_accept = slowev;
> +       if(ub_event_add(b->eb->slow_accept, &tv) != 0) {
> +               /* we do not want to log here,
> +                * error: "event_add failed." */
> +       }
> +}
> +
>  int comm_point_perform_accept(struct comm_point* c,
>         struct sockaddr_storage* addr, socklen_t* addrlen)
>  {
> @@ -3000,6 +3070,14 @@ int comm_point_perform_accept(struct com
>                         if(c->ev->base->stop_accept) {
>                                 struct comm_base* b = c->ev->base;
>                                 struct timeval tv;
> +                               struct ub_event* slowev = ub_event_new(
> +                                       b->eb->base, -1, UB_EV_TIMEOUT,
> +                                       comm_base_handle_slow_accept, b);
> +                               if(!slowev) {
> +                                       verbose(VERB_ALGO, "slow accept: "
> +                                               "event_new failed");
> +                                       return -1;
> +                               }
>                                 verbose(VERB_ALGO, "out of file
> descriptors: "
>                                         "slow accept");
>                                 ub_comm_base_now(b);
> @@ -3019,15 +3097,8 @@ int comm_point_perform_accept(struct com
>                                 /* set timeout, no mallocs */
>                                 tv.tv_sec = NETEVENT_SLOW_ACCEPT_TIME/1000;
>                                 tv.tv_usec =
> (NETEVENT_SLOW_ACCEPT_TIME%1000)*1000;
> -                               b->eb->slow_accept =
> ub_event_new(b->eb->base,
> -                                       -1, UB_EV_TIMEOUT,
> -                                       comm_base_handle_slow_accept, b);
> -                               if(b->eb->slow_accept == NULL) {
> -                                       /* we do not want to log here,
> because
> -                                        * that would spam the logfiles.
> -                                        * error: "event_base_set failed."
> */
> -                               }
> -                               else if(ub_event_add(b->eb->slow_accept,
> &tv)
> +                               b->eb->slow_accept = slowev;
> +                               if(ub_event_add(b->eb->slow_accept, &tv)
>                                         != 0) {
>                                         /* we do not want to log here,
>                                          * error: "event_add failed." */
> @@ -3159,6 +3230,26 @@ static int http2_submit_settings(struct
>  }
>  #endif /* HAVE_NGHTTP2 */
>
> +/** Clear http2 stream mesh states */
> +static void http2_session_clear_meshstate(struct http2_session*
> h2_session)
> +{
> +#ifdef HAVE_NGHTTP2
> +       /* Since the session gets closed, remove the mesh state
> references. */
> +       struct http2_stream* h2_stream;
> +       for(h2_stream = h2_session->first_stream; h2_stream;
> +               h2_stream = h2_stream->next) {
> +               if(h2_stream->mesh_state) {
> +                       mesh_state_remove_reply(h2_stream->mesh,
> +                               h2_stream->mesh_state, h2_session->c,
> +                               h2_stream, NULL);
> +                       h2_stream->mesh_state = NULL;
> +               }
> +       }
> +#else
> +       (void)h2_session;
> +#endif /* HAVE_NGHTTP2 */
> +}
> +
>  #ifdef HAVE_NGHTTP2
>  /** Delete http2 stream. After session delete or stream close callback */
>  static void http2_stream_delete(struct http2_session* h2_session,
> @@ -3166,7 +3257,7 @@ static void http2_stream_delete(struct h
>  {
>         if(h2_stream->mesh_state) {
>                 mesh_state_remove_reply(h2_stream->mesh,
> h2_stream->mesh_state,
> -                       h2_session->c, NULL);
> +                       h2_session->c, h2_stream, NULL);
>                 h2_stream->mesh_state = NULL;
>         }
>         http2_req_stream_clear(h2_stream);
> @@ -3208,6 +3299,13 @@ comm_point_tcp_accept_callback(int fd, s
>         /* find free tcp handler. */
>         if(!c->tcp_free) {
>                 log_warn("accepted too many tcp, connections full");
> +               /* Wait for a short moment (say 50msec) so that other
> +                * TCP connections can complete. Or timeout, at the busy
> +                * timeout of about 200msec. That stops this routine from
> +                * spinning endlessly, and gives time to complete the other
> +                * requests. But it is not as slow as the 2000msec wait
> +                * time for when the kernel is out of buffers. */
> +               comm_point_slow_accept(c, NETEVENT_SLOW_ACCEPT_QUEUE_TIME);
>                 return;
>         }
>         /* accept incoming connection. */
> @@ -3229,6 +3327,7 @@ comm_point_tcp_accept_callback(int fd, s
>                 if(!c_hdl->h2_session ||
>                         !http2_session_server_create(c_hdl->h2_session)) {
>                         log_warn("failed to create nghttp2");
> +                       comm_point_slow_accept(c, 0);
>                         return;
>                 }
>                 if(!c_hdl->h2_session ||
> @@ -3236,6 +3335,7 @@ comm_point_tcp_accept_callback(int fd, s
>                         log_warn("failed to submit http2 settings");
>                         if(c_hdl->h2_session)
>
> http2_session_server_delete(c_hdl->h2_session);
> +                       comm_point_slow_accept(c, 0);
>                         return;
>                 }
>                 if(!c->ssl) {
> @@ -3252,11 +3352,12 @@ comm_point_tcp_accept_callback(int fd, s
>                         comm_point_tcp_handle_callback, c_hdl);
>         }
>         if(!c_hdl->ev->ev) {
> -               log_warn("could not ub_event_new, dropped tcp");
> +               log_warn("could not ub_event_new, for new tcp");
>  #ifdef HAVE_NGHTTP2
>                 if(c_hdl->type == comm_http && c_hdl->h2_session)
>                         http2_session_server_delete(c_hdl->h2_session);
>  #endif
> +               comm_point_slow_accept(c, 0);
>                 return;
>         }
>         log_assert(fd != -1);
> @@ -3270,6 +3371,10 @@ comm_point_tcp_accept_callback(int fd, s
>  #endif
>                 return;
>         }
> +       /* move per-netblock TCP-connection-limit handle to the handler so
> that
> +        * comm_point_close() on the handler decrements the count on close
> */
> +       c_hdl->tcl_addr = c->tcl_addr;
> +       c->tcl_addr = NULL;
>         /* Copy remote_address to client_address.
>          * Simplest way/time for streams to do that. */
>         c_hdl->repinfo.client_addrlen = c_hdl->repinfo.remote_addrlen;
> @@ -4172,8 +4277,8 @@ recv_error:
>         if(errno == EINTR || errno == EAGAIN)
>                 return 1;
>  #ifdef ECONNRESET
> -               if(errno == ECONNRESET && verbosity < 2)
> -                       return 0; /* silence reset by peer */
> +       if(errno == ECONNRESET && verbosity < 2)
> +               return 0; /* silence reset by peer */
>  #endif
>         if(recv_initial) {
>  #ifdef ECONNREFUSED
> @@ -4540,6 +4645,10 @@ comm_point_tcp_handle_write(int fd, stru
>  static int
>  tcp_req_info_read_again(int fd, struct comm_point* c)
>  {
> +       /* One event-loop visit drains at most this many pipelined queries;
> +        * the rest is re-queued, so that other file descriptors get
> +        * serviced in between. */
> +       int budget = NUM_TCP_PER_SELECT;
>         while(c->tcp_req_info->read_again) {
>                 int r;
>                 c->tcp_req_info->read_again = 0;
> @@ -4556,6 +4665,16 @@ tcp_req_info_read_again(int fd, struct c
>                         }
>                         return 0;
>                 }
> +               if(--budget <= 0 && c->tcp_req_info->read_again) {
> +                       /* Defer the rest of the drain to the next loop
> turn.
> +                        * This uses a zero delay timer. For TLS the
> undrained
> +                        * remainder sits in OpenSSL's user-space buffer.
> */
> +                       struct timeval tv;
> +                       memset(&tv, 0, sizeof(tv));
> +                       verbose(VERB_ALGO, "Defer tcp_req_info read
> again");
> +                       comm_timer_set(c->tcp_req_info->read_again_timer,
> &tv);
> +                       return 1;
> +               }
>         }
>         return 1;
>  }
> @@ -4569,6 +4688,7 @@ tcp_more_read_again(int fd, struct comm_
>         /* this continues until the read routines get EAGAIN or so,
>          * and thus does not call the callback, and the bool is 0 */
>         int* moreread = c->tcp_more_read_again;
> +       int budget = NUM_TCP_PER_SELECT;
>         while(moreread && *moreread) {
>                 *moreread = 0;
>                 if(!comm_point_tcp_handle_read(fd, c, 0)) {
> @@ -4581,6 +4701,30 @@ tcp_more_read_again(int fd, struct comm_
>                         }
>                         return;
>                 }
> +               if(--budget <= 0 && *moreread) {
> +                       /* Defer the rest of the drain to the next loop
> turn.
> +                        * This uses a zero delay timer. For TLS the
> undrained
> +                        * remainder sits in OpenSSL's user-space buffer.
> */
> +                       struct timeval tv;
> +                       memset(&tv, 0, sizeof(tv));
> +                       if(!c->tcp_more_read_again_timer) {
> +                               c->tcp_more_read_again_timer =
> comm_timer_create(c->ev->base, tcp_more_read_again_cb, c);
> +                               if(!c->tcp_more_read_again_timer) {
> +                                       log_err("out of memory for tcp
> more read again timer");
> +                                       reclaim_tcp_handler(c);
> +                                       if(!c->tcp_do_close) {
> +
>  fptr_ok(fptr_whitelist_comm_point(
> +                                                       c->callback));
> +                                               (void)(*c->callback)(c,
> c->cb_arg,
> +                                                       NETEVENT_CLOSED,
> NULL);
> +                                       }
> +                                       return;
> +                               }
> +                       }
> +                       verbose(VERB_ALGO, "Defer more read again");
> +                       comm_timer_set(c->tcp_more_read_again_timer, &tv);
> +                       return;
> +               }
>         }
>  }
>
> @@ -4609,6 +4753,23 @@ tcp_more_write_again(int fd, struct comm
>  }
>
>  void
> +tcp_read_again_cb(void* arg)
> +{
> +       struct tcp_req_info* req = (struct tcp_req_info*)arg;
> +       verbose(VERB_ALGO, "tcp_read_again_cb");
> +       if(!tcp_req_info_read_again(req->cp->fd, req->cp))
> +               return;
> +}
> +
> +void
> +tcp_more_read_again_cb(void* arg)
> +{
> +       struct comm_point* c = (struct comm_point*)arg;
> +       verbose(VERB_ALGO, "tcp_more_read_again_cb");
> +       tcp_more_read_again(c->fd, c);
> +}
> +
> +void
>  comm_point_tcp_handle_callback(int fd, short event, void* arg)
>  {
>         struct comm_point* c = (struct comm_point*)arg;
> @@ -5014,6 +5175,14 @@ http_chunked_segment(struct comm_point*
>                 c->http_stored = 0;
>                 sldns_buffer_skip(c->buffer, (ssize_t)c->tcp_byte_count);
>                 sldns_buffer_clear(c->http_temp);
> +               if(sldns_buffer_remaining(c->buffer) >
> +                       sldns_buffer_capacity(c->http_temp)) {
> +                       verbose(VERB_OPS, "http chunked: surplus %d
> exceeds "
> +                               "temp buffer %d",
> (int)sldns_buffer_remaining(
> +                               c->buffer), (int)sldns_buffer_capacity(
> +                               c->http_temp));
> +                       return 0;
> +               }
>                 sldns_buffer_write(c->http_temp,
>                         sldns_buffer_current(c->buffer),
>                         sldns_buffer_remaining(c->buffer));
> @@ -5344,6 +5513,13 @@ comm_point_http_handle_read(int fd, stru
>                 if(c->http_in_headers || c->http_in_chunk_headers) {
>                         /* if header is done, process the header */
>                         if(!http_header_done(c->buffer)) {
> +                               if(sldns_buffer_limit(c->buffer) ==
> +                                       sldns_buffer_capacity(c->buffer)) {
> +                                       verbose(VERB_OPS, "http header
> line "
> +                                               "exceeds %d bytes,
> transfer "
> +                                               "failed",
> (int)sldns_buffer_capacity(c->buffer));
> +                                       return 0;
> +                               }
>                                 /* copy remaining data to front of buffer
>                                  * and set rest for writing into it */
>                                 http_moveover_buffer(c->buffer);
> @@ -6035,7 +6211,7 @@ comm_point_create_tcp_handler(struct com
>         c->pp2_enabled = parent->pp2_enabled;
>         c->pp2_header_state = pp2_header_none;
>         if(spoolbuf) {
> -               c->tcp_req_info = tcp_req_info_create(spoolbuf);
> +               c->tcp_req_info = tcp_req_info_create(base, spoolbuf);
>                 if(!c->tcp_req_info) {
>                         log_err("could not create tcp commpoint");
>                         sldns_buffer_free(c->buffer);
> @@ -6584,7 +6760,10 @@ comm_point_close(struct comm_point* c)
>                         c->event_added = 0;
>                 }
>         }
> -       tcl_close_connection(c->tcl_addr);
> +       if(c->tcl_addr) {
> +               tcl_close_connection(c->tcl_addr);
> +               c->tcl_addr = NULL;
> +       }
>         if(c->tcp_req_info)
>                 tcp_req_info_clear(c->tcp_req_info);
>         if(c->h2_session)
> @@ -6594,6 +6773,9 @@ comm_point_close(struct comm_point* c)
>                 *c->tcp_more_read_again = 0;
>         if(c->tcp_more_write_again && *c->tcp_more_write_again)
>                 *c->tcp_more_write_again = 0;
> +       if(c->tcp_more_read_again_timer &&
> +               comm_timer_is_set(c->tcp_more_read_again_timer))
> +               comm_timer_disable(c->tcp_more_read_again_timer);
>
>         /* close fd after removing from event lists, or epoll.. is messed
> up */
>         if(c->fd != -1 && !c->do_not_close) {
> @@ -6633,6 +6815,7 @@ comm_point_delete(struct comm_point* c)
>                 free(c->tcp_handlers);
>         }
>         free(c->timeout);
> +       comm_timer_delete(c->tcp_more_read_again_timer);
>         if(c->type == comm_tcp || c->type == comm_local || c->type ==
> comm_http) {
>                 sldns_buffer_free(c->buffer);
>  #ifdef USE_DNSCRYPT
> @@ -6773,6 +6956,7 @@ comm_point_drop_reply(struct comm_reply*
>         if(repinfo->c->type == comm_http) {
>                 if(repinfo->c->h2_session) {
>                         repinfo->c->h2_session->is_drop = 1;
> +
>  http2_session_clear_meshstate(repinfo->c->h2_session);
>                         if(!repinfo->c->h2_session->postpone_drop)
>                                 reclaim_http_handler(repinfo->c);
>                         return;
> Index: util/netevent.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/netevent.h,v
> diff -u -p -r1.27 netevent.h
> --- util/netevent.h     27 Jul 2026 14:14:39 -0000      1.27
> +++ util/netevent.h     20 Sep 2026 09:50:48 -0000
> @@ -111,6 +111,8 @@ typedef int comm_point_callback_type(str
>
>  /** timeout to slow accept calls when not possible, in msec. */
>  #define NETEVENT_SLOW_ACCEPT_TIME 2000
> +/** timeout to slow accept calls when tcp queue is full, in msec. */
> +#define NETEVENT_SLOW_ACCEPT_QUEUE_TIME 50
>  /** timeout to slow down log print, so it does not spam the logs, in sec
> */
>  #define SLOW_LOG_TIME 10
>  /** for doq, the maximum dcid length, in ngtcp2 it is 20. */
> @@ -382,6 +384,9 @@ struct comm_point {
>          * Or leave NULL if it is not used at all. */
>         int* tcp_more_write_again;
>
> +       /** resume timer for tcp_more_read_again */
> +       struct comm_timer* tcp_more_read_again_timer;
> +
>         /** if set, read/write completes:
>                 read/write state of tcp is toggled.
>                 buffer reset/bytecount reset.
> @@ -1130,6 +1135,12 @@ void doq_send_pkt(struct comm_point* c,
>
>  /** doq timer callback function. */
>  void doq_timer_cb(void* arg);
> +
> +/** tcp read again callback function. For tcp req info listen. */
> +void tcp_read_again_cb(void* arg);
> +
> +/** tcp more read again callback function. For outside network. */
> +void tcp_more_read_again_cb(void* arg);
>
>  /**
>   * This routine is published for checks and tests, and is only used
> internally.
> Index: util/proxy_protocol.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/proxy_protocol.c,v
> diff -u -p -r1.1.1.3 proxy_protocol.c
> --- util/proxy_protocol.c       4 Sep 2024 09:35:36 -0000       1.1.1.3
> +++ util/proxy_protocol.c       20 Sep 2026 09:50:48 -0000
> @@ -185,14 +185,23 @@ pp2_read_header(uint8_t* buf, size_t buf
>                 (header->ver_cmd & 0xF) != PP2_CMD_PROXY) {
>                 return PP_PARSE_UNKNOWN_CMD;
>         }
> -       /* Check for supported family and protocol */
> -       if(header->fam_prot != PP2_UNSPEC_UNSPEC &&
> -               header->fam_prot != PP2_INET_STREAM &&
> -               header->fam_prot != PP2_INET_DGRAM &&
> -               header->fam_prot != PP2_INET6_STREAM &&
> -               header->fam_prot != PP2_INET6_DGRAM &&
> -               header->fam_prot != PP2_UNIX_STREAM &&
> -               header->fam_prot != PP2_UNIX_DGRAM) {
> +       /* Check for supported family and protocol, and that len covers
> +        * the per-family address block (proxy-protocol.txt s2.2). */
> +       switch(header->fam_prot) {
> +       case PP2_UNSPEC_UNSPEC:
> +               break;
> +       case PP2_INET_STREAM:
> +       case PP2_INET_DGRAM:
> +               if(ntohs(header->len) < PP2_HEADER_LEN_INET)
> +                       return PP_PARSE_SIZE;
> +               break;
> +       case PP2_INET6_STREAM:
> +       case PP2_INET6_DGRAM:
> +               if(ntohs(header->len) < PP2_HEADER_LEN_INET6)
> +                       return PP_PARSE_SIZE;
> +               break;
> +       default:
> +               /* PP2_UNIX_STREAM, PP2_UNIX_DGRAM, others. */
>                 return PP_PARSE_UNKNOWN_FAM_PROT;
>         }
>         /* We have a correct header */
> Index: util/proxy_protocol.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/proxy_protocol.h,v
> diff -u -p -r1.1.1.2 proxy_protocol.h
> --- util/proxy_protocol.h       12 Apr 2024 15:44:28 -0000      1.1.1.2
> +++ util/proxy_protocol.h       20 Sep 2026 09:50:48 -0000
> @@ -54,6 +54,15 @@
>  /** PROXYv2 version (protocol value) */
>  #define PP2_VERSION 0x2
>
> +/** PROXYv2 minimum header.len value for TCP/UDP over IPv4 */
> +#define PP2_HEADER_LEN_INET 12
> +
> +/** PROXYv2 minimum header.len value for TCP/UDP over IPv6 */
> +#define PP2_HEADER_LEN_INET6 36
> +
> +/** PROXYv2 minimum header.len value for TCP/UDP over AF_UNIX */
> +#define PP2_HEADER_LEN_UNIX 216
> +
>  /**
>   * PROXYv2 command (protocol value).
>   */
> Index: util/tube.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/tube.c,v
> diff -u -p -r1.10 tube.c
> --- util/tube.c 31 Aug 2025 21:41:10 -0000      1.10
> +++ util/tube.c 20 Sep 2026 09:50:48 -0000
> @@ -145,6 +145,20 @@ void tube_remove_bg_write(struct tube* t
>         }
>  }
>
> +/** Drain the pipe of bytes. */
> +static void
> +fd_drain(int fd, uint32_t len)
> +{
> +       uint8_t discard[256];
> +       uint32_t remaining = len;
> +       while(remaining > 0) {
> +               ssize_t n = read(fd, discard,
> +                       remaining < sizeof(discard) ? remaining :
> sizeof(discard));
> +               if(n <= 0) break;
> +               remaining -= (uint32_t)n;
> +       }
> +}
> +
>  int
>  tube_handle_listen(struct comm_point* c, void* arg, int error,
>          struct comm_reply* ATTR_UNUSED(reply_info))
> @@ -184,6 +198,9 @@ tube_handle_listen(struct comm_point* c,
>                 tube->cmd_msg = (uint8_t*)calloc(1, tube->cmd_len);
>                 if(!tube->cmd_msg) {
>                         log_err("malloc failure");
> +                       /* Drain the remaining bytes, since they belong to
> this
> +                        * message. The next message starts after it. */
> +                       fd_drain(c->fd, tube->cmd_len);
>                         tube->cmd_read = 0;
>                         return 0;
>                 }
> @@ -374,6 +391,9 @@ int tube_read_msg(struct tube* tube, uin
>         *buf = (uint8_t*)malloc(*len);
>         if(!*buf) {
>                 log_err("tube read out of memory");
> +               /* Drain the remaining bytes, since they belong to this
> +                * message. The next message starts after it. */
> +               fd_drain(fd, *len);
>                 (void)fd_set_nonblock(fd);
>                 return 0;
>         }
> Index: util/data/dname.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/dname.c,v
> diff -u -p -r1.9 dname.c
> --- util/data/dname.c   26 Sep 2025 07:32:37 -0000      1.9
> +++ util/data/dname.c   20 Sep 2026 09:50:48 -0000
> @@ -192,34 +192,34 @@ pkt_dname_len(sldns_buffer* pkt)
>         while(1) {
>                 /* read next label */
>                 if(sldns_buffer_remaining(pkt) < 1)
> -                       return 0;
> +                       goto fail;
>                 labellen = sldns_buffer_read_u8(pkt);
>                 if(LABEL_IS_PTR(labellen)) {
>                         /* compression ptr */
>                         uint16_t ptr;
>                         if(sldns_buffer_remaining(pkt) < 1)
> -                               return 0;
> +                               goto fail;
>                         ptr = PTR_OFFSET(labellen,
> sldns_buffer_read_u8(pkt));
>                         if(ptrcount++ > MAX_COMPRESS_PTRS)
> -                               return 0; /* loop! */
> +                               goto fail; /* loop! */
>                         if(sldns_buffer_limit(pkt) <= ptr)
> -                               return 0; /* out of bounds! */
> +                               goto fail; /* out of bounds! */
>                         if(!endpos)
>                                 endpos = sldns_buffer_position(pkt);
>                         sldns_buffer_set_position(pkt, ptr);
>                 } else {
>                         /* label contents */
>                         if(labellen > 0x3f)
> -                               return 0; /* label too long */
> +                               goto fail; /* label too long */
>                         len += 1 + labellen;
>                         if(len > LDNS_MAX_DOMAINLEN)
> -                               return 0;
> +                               goto fail;
>                         if(labellen == 0) {
>                                 /* end of dname */
>                                 break;
>                         }
>                         if(sldns_buffer_remaining(pkt) < labellen)
> -                               return 0;
> +                               goto fail;
>                         sldns_buffer_skip(pkt, (ssize_t)labellen);
>                 }
>         }
> @@ -227,6 +227,13 @@ pkt_dname_len(sldns_buffer* pkt)
>                 sldns_buffer_set_position(pkt, endpos);
>
>         return len;
> +fail:
> +       /* Restore the position on failure too: callers (rdata_copy)
> compute
> +        * the consumed field length from the buffer position and must not
> +        * see a partial walk of a name that failed to parse. */
> +       if(endpos)
> +               sldns_buffer_set_position(pkt, endpos);
> +       return 0;
>  }
>
>  int
> Index: util/data/msgencode.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/msgencode.c,v
> diff -u -p -r1.18 msgencode.c
> --- util/data/msgencode.c       26 May 2026 11:14:11 -0000      1.18
> +++ util/data/msgencode.c       20 Sep 2026 09:50:48 -0000
> @@ -634,7 +634,7 @@ insert_query(struct query_info* qinfo, s
>         size_t qname_len = qinfo->local_alias ?
>                 qinfo->local_alias->rrset->rk.dname_len : qinfo->qname_len;
>         if(sldns_buffer_remaining(buffer) <
> -               qinfo->qname_len+sizeof(uint16_t)*2)
> +               qname_len+sizeof(uint16_t)*2)
>                 return RETVAL_TRUNC; /* buffer too small */
>         /* the query is the first name inserted into the tree */
>         if(!compress_tree_store(qname, dname_count_labels(qname),
> @@ -1129,9 +1129,11 @@ extended_error_encode(sldns_buffer* buf,
>         sldns_buffer_write(buf, &flags, sizeof(uint16_t));
>         sldns_buffer_write(buf, &flags, sizeof(uint16_t));
>         if(qinfo) {
> -               const uint8_t* qname = qinfo->local_alias ?
> +               const uint8_t* qname =
> +                       (qinfo->local_alias && qinfo->local_alias->rrset) ?
>                         qinfo->local_alias->rrset->rk.dname : qinfo->qname;
> -               size_t qname_len = qinfo->local_alias ?
> +               size_t qname_len =
> +                       (qinfo->local_alias && qinfo->local_alias->rrset) ?
>                         qinfo->local_alias->rrset->rk.dname_len :
>                         qinfo->qname_len;
>                 if(sldns_buffer_current(buf) == qname)
> Index: util/data/msgparse.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/msgparse.c,v
> diff -u -p -r1.15 msgparse.c
> --- util/data/msgparse.c        27 Jul 2026 14:14:39 -0000      1.15
> +++ util/data/msgparse.c        20 Sep 2026 09:50:48 -0000
> @@ -1033,8 +1033,11 @@ parse_edns_options_from_query(uint8_t* r
>                         break;
>
>                 case LDNS_EDNS_PADDING:
> -                       if(!cfg || !cfg->pad_responses ||
> -                                       !c || c->type != comm_tcp
> ||!c->ssl || padding_seen)
> +                       if(!cfg || !cfg->pad_responses || !c ||
> padding_seen)
> +                               break;
> +                       if(!((c->type == comm_tcp && c->ssl) ||
> +                               (c->type == comm_http && c->ssl) ||
> +                               c->type == comm_doq))
>                                 break;
>                         padding_seen = 1;
>                         if(!edns_opt_list_append(&edns->opt_list_out,
> @@ -1089,10 +1092,10 @@ parse_edns_options_from_query(uint8_t* r
>                                         cookie_is_v4, server_cookie, now);
>                         } else {
>                                 /* Use the cookie option value to
> validate. */
> -                       cookie_val_status = edns_cookie_server_validate(
> -                               rdata_ptr, opt_len, cfg->cookie_secret,
> -                               cfg->cookie_secret_len, cookie_is_v4,
> -                               server_cookie, now);
> +                               cookie_val_status =
> edns_cookie_server_validate(
> +                                       rdata_ptr, opt_len,
> cfg->cookie_secret,
> +                                       cfg->cookie_secret_len,
> cookie_is_v4,
> +                                       server_cookie, now);
>                         }
>                         if(cookie_val_status == COOKIE_STATUS_VALID_RENEW)
>                                 edns->cookie_valid = 1;
> @@ -1133,8 +1136,8 @@ parse_edns_options_from_query(uint8_t* r
>                                                 cookie_is_v4, now);
>
> lock_basic_unlock(&cookie_secrets->lock);
>                                 } else {
> -                               edns_cookie_server_write(server_cookie,
> -                                       cfg->cookie_secret, cookie_is_v4,
> now);
> +
>  edns_cookie_server_write(server_cookie,
> +                                               cfg->cookie_secret,
> cookie_is_v4, now);
>                                 }
>
> if(!edns_opt_list_append(&edns->opt_list_out,
>                                         LDNS_EDNS_COOKIE, 24,
> server_cookie,
> Index: util/data/msgreply.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/msgreply.c,v
> diff -u -p -r1.29 msgreply.c
> --- util/data/msgreply.c        26 May 2026 11:14:11 -0000      1.29
> +++ util/data/msgreply.c        20 Sep 2026 09:50:48 -0000
> @@ -248,6 +248,7 @@ rdata_copy(sldns_buffer* pkt, struct pac
>         sldns_pkt_section section)
>  {
>         uint16_t pkt_len;
> +       size_t tolen;
>         uint32_t ttl;
>         const sldns_rr_descriptor* desc;
>
> @@ -293,9 +294,13 @@ rdata_copy(sldns_buffer* pkt, struct pac
>                 (rr->ttl_data - sldns_buffer_begin(pkt) +
> sizeof(uint32_t)));
>         /* insert decompressed size into rdata len stored in memory */
>         /* -2 because rdatalen bytes are not included. */
> +       tolen = rr->size;
> +       if(tolen < 2)
> +               return 0;
>         pkt_len = htons(rr->size - 2);
>         memmove(to, &pkt_len, sizeof(uint16_t));
>         to += 2;
> +       tolen -= 2;
>         /* read packet rdata len */
>         pkt_len = sldns_buffer_read_u16(pkt);
>         if(sldns_buffer_remaining(pkt) < pkt_len)
> @@ -304,16 +309,29 @@ rdata_copy(sldns_buffer* pkt, struct pac
>         if(pkt_len > 0 && desc && desc->_dname_count > 0) {
>                 int count = (int)desc->_dname_count;
>                 int rdf = 0;
> -               size_t len;
> -               size_t oldpos;
> +               size_t len, dlen;
> +               size_t oldpos, newpos;
>                 /* decompress dnames. */
>                 while(pkt_len > 0 && count) {
>                         switch(desc->_wireformat[rdf]) {
>                         case LDNS_RDF_TYPE_DNAME:
>                                 oldpos = sldns_buffer_position(pkt);
> -                               dname_pkt_copy(pkt, to,
> +                               dlen = pkt_dname_len(pkt);
> +                               if(dlen == 0)
> +                                       return 0; /* malformed */
> +                               if(dlen > tolen)
> +                                       return 0; /* alloc mismatch */
> +                               newpos = sldns_buffer_position(pkt);
> +                               if(oldpos > newpos)
> +                                       return 0; /* should have moved
> forward*/
> +                               sldns_buffer_set_position(pkt, oldpos);
> +                               dname_pkt_copy(pkt, to,
>                                         sldns_buffer_current(pkt));
> -                               to += pkt_dname_len(pkt);
> +                               sldns_buffer_set_position(pkt, newpos);
> +                               to += dlen;
> +                               tolen -= dlen;
> +                               if(sldns_buffer_position(pkt)-oldpos >
> pkt_len)
> +                                       return 0; /* malformed: walks
> diverged */
>                                 pkt_len -=
> sldns_buffer_position(pkt)-oldpos;
>                                 count--;
>                                 len = 0;
> @@ -326,9 +344,12 @@ rdata_copy(sldns_buffer* pkt, struct pac
>                                 break;
>                         }
>                         if(len) {
> +                               if(len > tolen)
> +                                       return 0; /* alloc mismatch */
>                                 log_assert(len <= pkt_len);
>                                 memmove(to, sldns_buffer_current(pkt),
> len);
>                                 to += len;
> +                               tolen -= len;
>                                 sldns_buffer_skip(pkt, (ssize_t)len);
>                                 pkt_len -= len;
>                         }
> @@ -336,8 +357,11 @@ rdata_copy(sldns_buffer* pkt, struct pac
>                 }
>         }
>         /* copy remaining rdata */
> -       if(pkt_len >  0)
> +       if(pkt_len >  0) {
> +               if(pkt_len > tolen)
> +                       return 0; /* alloc mismatch */
>                 memmove(to, sldns_buffer_current(pkt), pkt_len);
> +       }
>
>         return 1;
>  }
> @@ -483,9 +507,12 @@ parse_copy_decompress_rrset(sldns_buffer
>         }
>         pk->entry.data = (void*)data;
>         pk->entry.key = (void*)pk;
> -       pk->entry.hash = pset->hash;
> -       data->trust = get_rrset_trust(msg, pset);
>         pk->rk.flags |= (data->ttl == 0) ? PACKED_RRSET_UPSTREAM_0TTL : 0;
> +       if( (pk->rk.flags & PACKED_RRSET_UPSTREAM_0TTL) != 0)
> +               pk->entry.hash = rrset_key_hash(&pk->rk);
> +       else
> +               pk->entry.hash = pset->hash;
> +       data->trust = get_rrset_trust(msg, pset);
>         return 1;
>  }
>
> @@ -1112,6 +1139,17 @@ reply_all_rrsets_secure(struct reply_inf
>         return 1;
>  }
>
> +int reply_an_ns_rrsets_secure(struct reply_info* rep)
> +{
> +       size_t i;
> +       for(i=0; i<rep->an_numrrsets+rep->ns_numrrsets; i++) {
> +               if( ((struct packed_rrset_data*)rep->rrsets[i]->entry.data)
> +                       ->security != sec_status_secure )
> +               return 0;
> +       }
> +       return 1;
> +}
> +
>  struct reply_info*
>  parse_reply_in_temp_region(sldns_buffer* pkt, struct regional* region,
>         struct query_info* qi)
> @@ -1503,8 +1541,12 @@ struct edns_option* edns_opt_list_find(s
>  int local_alias_shallow_copy_qname(struct local_rrset* local_alias,
> uint8_t** qname,
>         size_t* qname_len)
>  {
> -       struct ub_packed_rrset_key* rrset = local_alias->rrset;
> -       struct packed_rrset_data* d = rrset->entry.data;
> +       struct ub_packed_rrset_key* rrset;
> +       struct packed_rrset_data* d;
> +       rrset = local_alias->rrset;
> +       if(!rrset) return 0;
> +       d = rrset->entry.data;
> +       if(!d) return 0;
>
>         /* Sanity check: our current implementation only supports
>             * a single CNAME RRset as a local alias. */
> Index: util/data/msgreply.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/msgreply.h,v
> diff -u -p -r1.19 msgreply.h
> --- util/data/msgreply.h        26 May 2026 11:14:11 -0000      1.19
> +++ util/data/msgreply.h        20 Sep 2026 09:50:48 -0000
> @@ -494,6 +494,9 @@ int reply_check_cname_chain(struct query
>   */
>  int reply_all_rrsets_secure(struct reply_info* rep);
>
> +/** Check status of answer and authority section RRs. */
> +int reply_an_ns_rrsets_secure(struct reply_info* rep);
> +
>  /**
>   * Find answer rrset in reply, the one matching qinfo. Follows CNAMEs, so
> the
>   * result may have a different owner name.
> Index: util/data/packed_rrset.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/packed_rrset.c,v
> diff -u -p -r1.9 packed_rrset.c
> --- util/data/packed_rrset.c    27 Jul 2026 14:14:39 -0000      1.9
> +++ util/data/packed_rrset.c    20 Sep 2026 09:50:48 -0000
> @@ -297,7 +297,7 @@ int packed_rr_to_string(struct ub_packed
>         wlen = (size_t)sldns_wire2str_rr_buf(rr, rlen, dest, dest_len);
>         if(wlen >= dest_len) {
>                 /* the output string was truncated */
> -               log_info("rrbuf failure %d %s", (int)d->rr_len[i], dest);
> +               verbose(VERB_ALGO, "rrbuf failure %d %s",
> (int)d->rr_len[i], dest);
>                 dest[0] = 0;
>                 return 0;
>         }
> @@ -363,8 +363,11 @@ packed_rrset_copy_region(struct ub_packe
>                  * of the novel ghost attack mitigation i.e., using the
>                  * qstarttime for NS RRSets. In that case make sure that
> the
>                  * returned TTL is not higher than the original one. */
> -               log_assert(d->ttl_add <= now ||
> -                       (ntohs(key->rk.type) == LDNS_RR_TYPE_NS));
> +               /* For types other than type NS, auth zone and rpz code
> +                * can have ttl_add values. Also time could conceivably
> move
> +                * in reverse, due to operator action, and it is prudent
> +                * to not assert on that here.
> +                * So there is no assertion d->ttl_add <= now || type==NS
> */
>                 now_control = SERVE_ORIGINAL_TTL ? data->ttl_add
>                         : (d->ttl_add > now ? d->ttl_add : now );
>                 for(i=0; i<d->count + d->rrsig_count; i++) {
> Index: util/data/packed_rrset.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/data/packed_rrset.h,v
> diff -u -p -r1.9 packed_rrset.h
> --- util/data/packed_rrset.h    26 May 2026 11:14:11 -0000      1.9
> +++ util/data/packed_rrset.h    20 Sep 2026 09:50:48 -0000
> @@ -72,6 +72,8 @@ typedef uint64_t rrset_id_type;
>  #define PACKED_RRSET_UNVERIFIED_GLUE 0x10
>  /** this rrset has a 0TTL from upstream */
>  #define PACKED_RRSET_UPSTREAM_0TTL 0x20
> +/** this rrset has 0TTL from upstream and also has had grace TTL applied
> */
> +#define PACKED_RRSET_0TTL_GRACE 0x40
>
>  /** number of rrs and rrsets for integer overflow protection.  More than
>   * this is not really possible (64K packet has much less RRs and RRsets)
> in
> Index: util/shm_side/shm_main.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/shm_side/shm_main.c,v
> diff -u -p -r1.9 shm_main.c
> --- util/shm_side/shm_main.c    26 May 2026 11:14:11 -0000      1.9
> +++ util/shm_side/shm_main.c    20 Sep 2026 09:50:48 -0000
> @@ -351,6 +351,8 @@ void shm_main_run(struct worker *worker)
>         int offset;
>         double total_mesh_time_median;
>         struct shm_main_info* shm_info = worker->daemon->shm_info;
> +       if(!shm_info)
> +               return;
>
>  #ifndef S_SPLINT_S
>         verbose(VERB_DETAIL, "SHM run - worker [%d] - daemon [%p] -
> timenow(%u) - timeboot(%u)",
> Index: util/storage/lookup3.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/util/storage/lookup3.c,v
> diff -u -p -r1.8 lookup3.c
> --- util/storage/lookup3.c      4 Sep 2024 09:36:41 -0000       1.8
> +++ util/storage/lookup3.c      20 Sep 2026 09:50:48 -0000
> @@ -255,10 +255,10 @@ uint32_t        initval)         /* the
>    {
>    case 3 : c+=k[2];
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>    case 2 : b+=k[1];
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>    case 1 : a+=k[0];
>      final(a,b,c);
>         ATTR_FALLTHROUGH
> @@ -531,37 +531,37 @@ uint32_t hashlittle( const void *key, si
>      {
>      case 12: c+=((uint32_t)k[11])<<24;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 11: c+=((uint32_t)k[10])<<16;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 10: c+=((uint32_t)k[9])<<8;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 9 : c+=k[8];
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 8 : b+=((uint32_t)k[7])<<24;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 7 : b+=((uint32_t)k[6])<<16;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 6 : b+=((uint32_t)k[5])<<8;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 5 : b+=k[4];
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 4 : a+=((uint32_t)k[3])<<24;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 3 : a+=((uint32_t)k[2])<<16;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 2 : a+=((uint32_t)k[1])<<8;
>         ATTR_FALLTHROUGH
> -       /* fallthrough */
> +       /* fallthrough */
>      case 1 : a+=k[0];
>               break;
>      case 0 : return c;
> Index: validator/autotrust.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/autotrust.c,v
> diff -u -p -r1.20 autotrust.c
> --- validator/autotrust.c       31 Aug 2025 21:41:10 -0000      1.20
> +++ validator/autotrust.c       20 Sep 2026 09:50:48 -0000
> @@ -160,10 +160,12 @@ verbose_key(struct autr_ta* ta, enum ver
>   * Parse comments
>   * @param str: to parse
>   * @param ta: trust key autotrust metadata
> + * @param header_seen: if an autotrust file header was seen.
> + *     Without such a header it is a list of resource records.
>   * @return false on failure.
>   */
>  static int
> -parse_comments(char* str, struct autr_ta* ta)
> +parse_comments(char* str, struct autr_ta* ta, int header_seen)
>  {
>          int len = (int)strlen(str), pos = 0, timestamp = 0;
>          char* comment = (char*) malloc(sizeof(char)*len+1);
> @@ -196,10 +198,18 @@ parse_comments(char* str, struct autr_ta
>                  free(comment);
>                  return 0;
>          }
> -        if (pos <= 0)
> -                ta->s = AUTR_STATE_VALID;
> -        else
> -        {
> +        if (pos <= 0) {
> +               if(header_seen) {
> +                       /* There was an autotrust trust anchor file header,
> +                        * with a ;; id=.. line, so the entries
> +                        * have to have ;;state= annotations. */
> +                       log_err("trust anchor in state file has no
> ;;state= "
> +                               "annotation, ignoring");
> +                       free(comment);
> +                       return 0;
> +               }
> +               ta->s = AUTR_STATE_VALID;
> +        } else {
>                  int s = (int) comments[pos] - '0';
>                  switch(s)
>                  {
> @@ -391,6 +401,15 @@ autr_rrset_delete(struct ub_packed_rrset
>         }
>  }
>
> +/** delete autotrust key data */
> +static void
> +autr_ta_delete(struct autr_ta* ta)
> +{
> +       if(!ta) return;
> +       free(ta->rr);
> +       free(ta);
> +}
> +
>  void autr_point_delete(struct trust_anchor* tp)
>  {
>         if(!tp)
> @@ -404,8 +423,7 @@ void autr_point_delete(struct trust_anch
>                 struct autr_ta* p = tp->autr->keys, *np;
>                 while(p) {
>                         np = p->next;
> -                       free(p->rr);
> -                       free(p);
> +                       autr_ta_delete(p);
>                         p = np;
>                 }
>                 free(tp->autr->file);
> @@ -449,8 +467,7 @@ add_trustanchor_frm_rr(struct val_anchor
>                 return NULL;
>         *tp = find_add_tp(anchors, rr, rr_len, dname_len);
>         if(!*tp) {
> -               free(ta->rr);
> -               free(ta);
> +               autr_ta_delete(ta);
>                 return NULL;
>         }
>         /* add ta to tp */
> @@ -523,12 +540,14 @@ add_trustanchor_frm_str(struct val_ancho
>   * @param prev: passed to ldns.
>   * @param prev_len: length of prev
>   * @param skip: if true, the result is NULL, but not an error, skip it.
> + * @param header_seen: if an autotrust file header was seen.
> + *     Without such a header it is a list of resource records.
>   * @return false on failure, otherwise the tp read.
>   */
>  static struct trust_anchor*
>  load_trustanchor(struct val_anchors* anchors, char* str, const char*
> fname,
>         uint8_t* origin, size_t origin_len, uint8_t** prev, size_t*
> prev_len,
> -       int* skip)
> +       int* skip, int header_seen)
>  {
>         struct autr_ta* ta = NULL;
>         struct trust_anchor* tp = NULL;
> @@ -538,7 +557,11 @@ load_trustanchor(struct val_anchors* anc
>         if(!ta)
>                 return NULL;
>         lock_basic_lock(&tp->lock);
> -       if(!parse_comments(str, ta)) {
> +       if(!parse_comments(str, ta, header_seen)) {
> +               /* ta was already linked into the list of keys, unlink it
> */
> +               log_assert(tp->autr->keys == ta);
> +               tp->autr->keys = ta->next;
> +               autr_ta_delete(ta);
>                 lock_basic_unlock(&tp->lock);
>                 return NULL;
>         }
> @@ -846,19 +869,32 @@ parse_id(struct val_anchors* anchors, ch
>   * @param anchors: the anchor is added to this, if "id:" is seen.
>   * @param anchor: the anchor as result value or previously returned anchor
>   *     value to read the variable lines into.
> + * @param header_seen: if a header ';;id: example.com.' was seen.
> + * @param nm: file name.
>   * @return: 0 no match, -1 failed syntax error, +1 success line read.
>   *     +2 revoked trust anchor file.
>   */
>  static int
>  parse_var_line(char* line, struct val_anchors* anchors,
> -       struct trust_anchor** anchor)
> +       struct trust_anchor** anchor, int* header_seen, const char* nm)
>  {
>         struct trust_anchor* tp = *anchor;
>         int r = 0;
>         if(strncmp(line, ";;id: ", 6) == 0) {
> +               *header_seen = 1;
>                 *anchor = parse_id(anchors, line+6);
>                 if(!*anchor) return -1;
> -               else return 1;
> +               lock_basic_lock(&(*anchor)->lock);
> +               if(*anchor && !(*anchor)->autr->file) {
> +                       (*anchor)->autr->file = strdup(nm);
> +                       if(!(*anchor)->autr->file) {
> +                               lock_basic_unlock(&(*anchor)->lock);
> +                               log_err("malloc failure");
> +                               return -1;
> +                       }
> +               }
> +               lock_basic_unlock(&(*anchor)->lock);
> +               if(*anchor) return 1;
>         } else if(strncmp(line, ";;REVOKED", 9) == 0) {
>                 if(tp) {
>                         log_err("REVOKED statement must be at start of
> file");
> @@ -992,14 +1028,15 @@ int autr_read_file(struct val_anchors* a
>          FILE* fd;
>          /* keep track of line numbers */
>          int line_nr = 0;
> -        /* single line */
> -        char line[10240];
> +        /* single line, enough space for large DNSKEY, 64K, in hex and
> dname */
> +        char line[10240+65536*2];
>         /* trust point being read */
>         struct trust_anchor *tp = NULL, *tp2;
>         int r;
>         /* for $ORIGIN parsing */
>         uint8_t *origin=NULL, *prev=NULL;
>         size_t origin_len=0, prev_len=0;
> +       int header_seen = 0;
>
>          if (!(fd = fopen(nm, "r"))) {
>                  log_err("unable to open %s for reading: %s",
> @@ -1008,7 +1045,7 @@ int autr_read_file(struct val_anchors* a
>          }
>          verbose(VERB_ALGO, "reading autotrust anchor file %s", nm);
>          while ( (r=read_multiline(line, sizeof(line), fd, &line_nr)) !=
> 0) {
> -               if(r == -1 || (r = parse_var_line(line, anchors, &tp)) ==
> -1) {
> +               if(r == -1 || (r = parse_var_line(line, anchors, &tp,
> &header_seen, nm)) == -1) {
>                         log_err("could not parse auto-trust-anchor-file "
>                                 "%s line %d", nm, line_nr);
>                         fclose(fd);
> @@ -1030,7 +1067,7 @@ int autr_read_file(struct val_anchors* a
>                         continue;
>                 r = 0;
>                  if(!(tp2=load_trustanchor(anchors, line, nm, origin,
> -                       origin_len, &prev, &prev_len, &r))) {
> +                       origin_len, &prev, &prev_len, &r, header_seen))) {
>                         if(!r) log_err("failed to load trust anchor from
> %s "
>                                 "at line %i, skipping", nm, line_nr);
>                          /* try to do the rest */
> @@ -1194,6 +1231,11 @@ void autr_write_file(struct module_env*
>  #endif
>         char tempf[2048];
>         log_assert(tp->autr);
> +       if(!fname) {
> +               log_err("autotrust: trust point has no backing file, "
> +                       "skipping write");
> +               return;
> +       }
>         if(!env) {
>                 log_err("autr_write_file: Module environment is NULL.");
>                 return;
> @@ -1255,12 +1297,13 @@ void autr_write_file(struct module_env*
>   * @param tp: trust point to verify with
>   * @param rrset: DNSKEY rrset to verify.
>   * @param qstate: qstate with region.
> + * @param vq: validator query state.
>   * @return false on failure, true if verification successful.
>   */
>  static int
>  verify_dnskey(struct module_env* env, struct val_env* ve,
>          struct trust_anchor* tp, struct ub_packed_rrset_key* rrset,
> -       struct module_qstate* qstate)
> +       struct module_qstate* qstate, struct val_qstate* vq)
>  {
>         char reasonbuf[256];
>         char* reason = NULL;
> @@ -1268,7 +1311,7 @@ verify_dnskey(struct module_env* env, st
>         int downprot = env->cfg->harden_algo_downgrade;
>         enum sec_status sec = val_verify_DNSKEY_with_TA(env, ve, rrset,
>                 tp->ds_rrset, tp->dnskey_rrset, downprot?sigalg:NULL,
> &reason,
> -               NULL, qstate, reasonbuf, sizeof(reasonbuf));
> +               NULL, qstate, vq, reasonbuf, sizeof(reasonbuf));
>         /* sigalg is ignored, it returns algorithms signalled to exist, but
>          * in 5011 there are no other rrsets to check.  if downprot is
>          * enabled, then it checks that the DNSKEY is signed with all
> @@ -1308,16 +1351,18 @@ min_expiry(struct module_env* env, struc
>  static int
>  rr_is_selfsigned_revoked(struct module_env* env, struct val_env* ve,
>         struct ub_packed_rrset_key* dnskey_rrset, size_t i,
> -       struct module_qstate* qstate)
> +       struct module_qstate* qstate, struct val_qstate* vq)
>  {
>         enum sec_status sec;
>         char* reason = NULL;
> +       size_t num_tagmatches = 0;
>         verbose(VERB_ALGO, "seen REVOKE flag, check self-signed, rr %d",
>                 (int)i);
>         /* no algorithm downgrade protection necessary, if it is selfsigned
>          * revoked it can be removed. */
>         sec = dnskey_verify_rrset(env, ve, dnskey_rrset, dnskey_rrset, i,
> -               &reason, NULL, LDNS_SECTION_ANSWER, qstate);
> +               &reason, NULL, LDNS_SECTION_ANSWER, qstate, vq,
> +               &num_tagmatches);
>         return (sec == sec_status_secure);
>  }
>
> @@ -1533,7 +1578,7 @@ init_events(struct trust_anchor* tp)
>  static void
>  check_contains_revoked(struct module_env* env, struct val_env* ve,
>         struct trust_anchor* tp, struct ub_packed_rrset_key* dnskey_rrset,
> -       int* changed, struct module_qstate* qstate)
> +       int* changed, struct module_qstate* qstate, struct val_qstate* vq)
>  {
>         struct packed_rrset_data* dd = (struct packed_rrset_data*)
>                 dnskey_rrset->entry.data;
> @@ -1553,7 +1598,8 @@ check_contains_revoked(struct module_env
>                 }
>                 if(!ta)
>                         continue; /* key not found */
> -               if(rr_is_selfsigned_revoked(env, ve, dnskey_rrset, i,
> qstate)) {
> +               if(rr_is_selfsigned_revoked(env, ve, dnskey_rrset, i,
> qstate,
> +                       vq)) {
>                         /* checked if there is an rrsig signed by this
> key. */
>                         /* same keytag, but stored can be revoked already,
> so
>                          * compare keytags, with +0 or +128(REVOKE flag) */
> @@ -1992,8 +2038,7 @@ autr_cleanup_keys(struct trust_anchor* t
>                         != LDNS_RR_TYPE_DNSKEY) {
>                         struct autr_ta* np = p->next;
>                         /* remove */
> -                       free(p->rr);
> -                       free(p);
> +                       autr_ta_delete(p);
>                         /* snip and go to next item */
>                         *prevp = np;
>                         p = np;
> @@ -2168,7 +2213,7 @@ autr_tp_remove(struct module_env* env, s
>
>  int autr_process_prime(struct module_env* env, struct val_env* ve,
>         struct trust_anchor* tp, struct ub_packed_rrset_key* dnskey_rrset,
> -       struct module_qstate* qstate)
> +       struct module_qstate* qstate, struct val_qstate* vq)
>  {
>         int changed = 0;
>         log_assert(tp && tp->autr);
> @@ -2209,7 +2254,7 @@ int autr_process_prime(struct module_env
>                 return 1; /* trust point exists */
>         }
>         /* check for revoked keys to remove immediately */
> -       check_contains_revoked(env, ve, tp, dnskey_rrset, &changed,
> qstate);
> +       check_contains_revoked(env, ve, tp, dnskey_rrset, &changed,
> qstate, vq);
>         if(changed) {
>                 verbose(VERB_ALGO, "autotrust: revokedkeys, reassemble");
>                 if(!autr_assemble(tp)) {
> @@ -2225,7 +2270,7 @@ int autr_process_prime(struct module_env
>                 }
>         }
>         /* verify the dnskey rrset and see if it is valid. */
> -       if(!verify_dnskey(env, ve, tp, dnskey_rrset, qstate)) {
> +       if(!verify_dnskey(env, ve, tp, dnskey_rrset, qstate, vq)) {
>                 verbose(VERB_ALGO, "autotrust: dnskey did not verify.");
>                 /* only increase failure count if this is not the first
> prime,
>                  * this means there was a previous successful probe */
> @@ -2318,7 +2363,7 @@ autr_debug_print_tp(struct trust_anchor*
>         if(tp->dnskey_rrset) {
>                 log_packed_rrset(NO_VERBOSE, "DNSKEY:", tp->dnskey_rrset);
>         }
> -       log_info("file %s", tp->autr->file);
> +       log_info("file %s", (tp->autr->file?tp->autr->file:"null"));
>         (void)autr_ctime_r(&tp->autr->last_queried, buf);
>         if(buf[0]) buf[strlen(buf)-1]=0; /* remove newline */
>         log_info("last_queried: %u %s", (unsigned)tp->autr->last_queried,
> buf);
> @@ -2416,7 +2461,7 @@ probe_anchor(struct module_env* env, str
>                 qinfo.qclass);
>
>         if(!mesh_new_callback(env->mesh, &qinfo, qflags, &edns, buf, 0,
> -               &probe_answer_cb, env, 0)) {
> +               &probe_answer_cb, env, 0, NULL)) {
>                 log_err("out of memory making 5011 probe");
>         }
>  }
> Index: validator/autotrust.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/autotrust.h,v
> diff -u -p -r1.4 autotrust.h
> --- validator/autotrust.h       20 Sep 2018 23:15:40 -0000      1.4
> +++ validator/autotrust.h       20 Sep 2026 09:50:48 -0000
> @@ -50,6 +50,7 @@ struct module_env;
>  struct module_qstate;
>  struct val_env;
>  struct sldns_buffer;
> +struct val_qstate;
>
>  /** Autotrust anchor states */
>  typedef enum {
> @@ -190,13 +191,14 @@ void autr_point_delete(struct trust_anch
>   * @param dnskey_rrset: DNSKEY rrset probed (can be NULL if bad prime
> result).
>   *     allocated in a region. Has not been validated yet.
>   * @param qstate: qstate with region.
> + * @param vq: validator query state.
>   * @return false if trust anchor was revoked completely.
>   *     Otherwise logs errors to log, does not change return value.
>   *     On errors, likely the trust point has been unchanged.
>   */
>  int autr_process_prime(struct module_env* env, struct val_env* ve,
>         struct trust_anchor* tp, struct ub_packed_rrset_key* dnskey_rrset,
> -       struct module_qstate* qstate);
> +       struct module_qstate* qstate, struct val_qstate* vq);
>
>  /**
>   * Debug printout of rfc5011 tracked anchors
> Index: validator/val_anchor.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_anchor.c,v
> diff -u -p -r1.11 val_anchor.c
> --- validator/val_anchor.c      31 Aug 2025 21:41:10 -0000      1.11
> +++ validator/val_anchor.c      20 Sep 2026 09:50:48 -0000
> @@ -534,7 +534,10 @@ readkeyword_bindfile(FILE* in, sldns_buf
>         while((c = getc(in)) != EOF ) {
>                 if(comments && c == '#') {      /*   # blabla   */
>                         skip_to_eol(in, &c);
> -                       if(c == EOF) return 0;
> +                       if(c == EOF) {
> +                               log_err("trusted-keys, %d, got EOF",
> *line);
> +                               return 0;
> +                       }
>                         (*line)++;
>                         continue;
>                 } else if(comments && c=='/' && numdone>0 && /* /_/ bla*/
> @@ -543,7 +546,10 @@ readkeyword_bindfile(FILE* in, sldns_buf
>                         sldns_buffer_skip(buf, -1);
>                         numdone--;
>                         skip_to_eol(in, &c);
> -                       if(c == EOF) return 0;
> +                       if(c == EOF) {
> +                               log_err("trusted-keys, %d, got EOF",
> *line);
> +                               return 0;
> +                       }
>                         (*line)++;
>                         continue;
>                 } else if(comments && c=='*' && numdone>0 && /* /_* bla
> *_/ */
> @@ -560,7 +566,10 @@ readkeyword_bindfile(FILE* in, sldns_buf
>                                 if(c == '\n')
>                                         (*line)++;
>                         }
> -                       if(c == EOF) return 0;
> +                       if(c == EOF) {
> +                               log_err("trusted-keys, %d, got EOF",
> *line);
> +                               return 0;
> +                       }
>                         continue;
>                 }
>                 /* not a comment, complete the keyword */
> @@ -581,7 +590,8 @@ readkeyword_bindfile(FILE* in, sldns_buf
>                 }
>                 /* space for 1 char + 0 string terminator */
>                 if(sldns_buffer_remaining(buf) < 2) {
> -                       fatal_exit("trusted-keys, %d, string too long",
> *line);
> +                       log_err("trusted-keys, %d, string too long",
> *line);
> +                       return 0;
>                 }
>                 sldns_buffer_write_u8(buf, (uint8_t)c);
>                 numdone++;
> @@ -595,7 +605,10 @@ readkeyword_bindfile(FILE* in, sldns_buf
>                                         break;
>                                 }
>                         }
> -                       if(c == EOF) return 0;
> +                       if(c == EOF) {
> +                               log_err("trusted-keys, %d, got EOF",
> *line);
> +                               return 0;
> +                       }
>                         return numdone;
>                 }
>                 if(is_bind_special(c))
> @@ -623,7 +636,7 @@ skip_to_special(FILE* in, sldns_buffer*
>                 }
>                 return 1;
>         }
> -       log_err("trusted-keys, line %d, expected %c got EOF", *line, spec);
> +       log_err("trusted-keys, line %d, expected %c, read failed", *line,
> spec);
>         return 0;
>  }
>
> Index: validator/val_neg.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_neg.c,v
> diff -u -p -r1.12 val_neg.c
> --- validator/val_neg.c 26 May 2026 11:14:11 -0000      1.12
> +++ validator/val_neg.c 20 Sep 2026 09:50:48 -0000
> @@ -938,6 +938,10 @@ void val_neg_addreply(struct val_neg_cac
>                         continue;
>                 if(!dname_subdomain_c(rep->rrsets[i]->rk.dname,
>                         zone->name)) continue;
> +               if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NSEC &&
> +                       !nsec_nextowner_subdomain(rep->rrsets[i],
> zone->name)) {
> +                       continue; /* nextowner not in zone */
> +               }
>                 /* insert NSEC into this zone's tree */
>                 neg_insert_data(neg, zone, rep->rrsets[i]);
>         }
> @@ -1022,6 +1026,10 @@ void val_neg_addreferral(struct val_neg_
>                         continue;
>                 if(!dname_subdomain_c(rep->rrsets[i]->rk.dname,
>                         zone->name)) continue;
> +               if(ntohs(rep->rrsets[i]->rk.type) == LDNS_RR_TYPE_NSEC &&
> +                       !nsec_nextowner_subdomain(rep->rrsets[i],
> zone->name)) {
> +                       continue; /* nextowner not in zone */
> +               }
>                 /* insert NSEC into this zone's tree */
>                 neg_insert_data(neg, zone, rep->rrsets[i]);
>         }
> @@ -1110,12 +1118,14 @@ grab_nsec(struct rrset_cache* rrset_cach
>   * @param rrset_cache: rrset cache
>   * @param now: to check ttl against
>   * @param region: where to alloc result
> + * @param topname: do not look higher than this name, so that the
> + *   result cannot be taken from a zone above the current trust anchor.
>   * @return rrset or NULL
>   */
>  static struct ub_packed_rrset_key*
>  neg_find_nsec(struct val_neg_cache* neg_cache, uint8_t* qname, size_t
> qname_len,
>         uint16_t qclass, struct rrset_cache* rrset_cache, time_t now,
> -       struct regional* region)
> +       struct regional* region, uint8_t* topname)
>  {
>         int labs;
>         uint32_t flags;
> @@ -1133,6 +1143,11 @@ neg_find_nsec(struct val_neg_cache* neg_
>                 lock_basic_unlock(&neg_cache->lock);
>                 return NULL;
>         }
> +       if(topname && !dname_subdomain_c(zone->name, topname)) {
> +               /* Reject NSEC not within trust anchor's bailiwick */
> +               lock_basic_unlock(&neg_cache->lock);
> +               return NULL;
> +       }
>
>         /* NSEC only for now */
>         if(zone->nsec3_hash) {
> @@ -1223,8 +1238,8 @@ neg_params_ok(struct val_neg_zone* zone,
>                 return 0;
>         return (h == zone->nsec3_hash && it == zone->nsec3_iter &&
>                 slen == zone->nsec3_saltlen &&
> -               (slen != 0 && zone->nsec3_salt && s
> -                 && memcmp(zone->nsec3_salt, s, slen) == 0));
> +               (slen == 0 || (slen != 0 && zone->nsec3_salt && s
> +                 && memcmp(zone->nsec3_salt, s, slen) == 0)));
>  }
>
>  /** get next closer for nsec3 proof */
> @@ -1313,7 +1328,7 @@ neg_nsec3_proof_ds(struct val_neg_zone*
>                         !nsec3_has_type(ce_rrset, 0, LDNS_RR_TYPE_NS))
>                         return NULL;
>                 if(!(msg = dns_msg_create(qname, qname_len,
> -                       LDNS_RR_TYPE_DS, zone->dclass, region, 1)))
> +                       LDNS_RR_TYPE_DS, zone->dclass, region, 2))) /* ce
> + soa */
>                         return NULL;
>                 /* The cache response means recursion is available. */
>                 msg->rep->flags |= BIT_RA;
> @@ -1430,7 +1445,7 @@ val_neg_getmsg(struct val_neg_cache* neg
>
>         /* Get best available NSEC for qname */
>         nsec = neg_find_nsec(neg, qinfo->qname, qinfo->qname_len,
> qinfo->qclass,
> -               rrset_cache, now, region);
> +               rrset_cache, now, region, topname);
>
>         /* Matching NSEC, use to generate No Data answer. Not creating
> answers
>          * yet for No Data proven using wildcard. */
> @@ -1510,7 +1525,7 @@ val_neg_getmsg(struct val_neg_cache* neg
>                                  * proof */
>                                 if(!(wcrr = neg_find_nsec(neg,
> wc_qinfo.qname,
>                                         wc_qinfo.qname_len, qinfo->qclass,
> -                                       rrset_cache, now, region)))
> +                                       rrset_cache, now, region,
> topname)))
>                                         return NULL;
>
>                                 nodata_wc = NULL;
> Index: validator/val_nsec.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec.c,v
> diff -u -p -r1.12 val_nsec.c
> --- validator/val_nsec.c        4 Sep 2024 09:36:41 -0000       1.12
> +++ validator/val_nsec.c        20 Sep 2026 09:50:48 -0000
> @@ -177,7 +177,8 @@ static int
>  nsec_verify_rrset(struct module_env* env, struct val_env* ve,
>         struct ub_packed_rrset_key* nsec, struct key_entry_key* kkey,
>         char** reason, sldns_ede_code* reason_bogus,
> -       struct module_qstate* qstate, char* reasonbuf, size_t reasonlen)
> +       struct module_qstate* qstate, struct val_qstate* vq, char*
> reasonbuf,
> +       size_t reasonlen)
>  {
>         struct packed_rrset_data* d = (struct packed_rrset_data*)
>                 nsec->entry.data;
> @@ -189,7 +190,7 @@ nsec_verify_rrset(struct module_env* env
>         if(d->security == sec_status_secure)
>                 return 1;
>         d->security = val_verify_rrset_entry(env, ve, nsec, kkey, reason,
> -               reason_bogus, LDNS_SECTION_AUTHORITY, qstate, &verified,
> +               reason_bogus, LDNS_SECTION_AUTHORITY, qstate, vq,
> &verified,
>                 reasonbuf, reasonlen);
>         if(d->security == sec_status_secure) {
>                 rrset_update_sec_status(env->rrset_cache, nsec, *env->now);
> @@ -203,7 +204,7 @@ val_nsec_prove_nodata_dsreply(struct mod
>         struct query_info* qinfo, struct reply_info* rep,
>         struct key_entry_key* kkey, time_t* proof_ttl, char** reason,
>         sldns_ede_code* reason_bogus, struct module_qstate* qstate,
> -       char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, char* reasonbuf, size_t reasonlen)
>  {
>         struct ub_packed_rrset_key* nsec = reply_find_rrset_section_ns(
>                 rep, qinfo->qname, qinfo->qname_len, LDNS_RR_TYPE_NSEC,
> @@ -221,26 +222,32 @@ val_nsec_prove_nodata_dsreply(struct mod
>          * 2) this is not a delegation point */
>         if(nsec) {
>                 if(!nsec_verify_rrset(env, ve, nsec, kkey, reason,
> -                       reason_bogus, qstate, reasonbuf, reasonlen)) {
> +                       reason_bogus, qstate, vq, reasonbuf, reasonlen)) {
>                         verbose(VERB_ALGO, "NSEC RRset for the "
>                                 "referral did not verify.");
>                         return sec_status_bogus;
>                 }
> -               sec = val_nsec_proves_no_ds(nsec, qinfo);
> -               if(sec == sec_status_bogus) {
> -                       /* something was wrong. */
> -                       *reason = "NSEC does not prove absence of DS";
> -                       *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> -                       return sec;
> -               } else if(sec == sec_status_insecure) {
> -                       /* this wasn't a delegation point. */
> -                       return sec;
> -               } else if(sec == sec_status_secure) {
> -                       /* this proved no DS. */
> -                       *proof_ttl = ub_packed_rrset_ttl(nsec);
> -                       return sec;
> +               /* If the NSEC was a wildcard, the verify rewrites the
> +                * owner to '*.zone'. Check the NSEC owner matches. */
> +               if(query_dname_compare(nsec->rk.dname, qinfo->qname) == 0)
> {
> +                       sec = val_nsec_proves_no_ds(nsec, qinfo);
> +                       if(sec == sec_status_bogus) {
> +                               /* something was wrong. */
> +                               *reason = "NSEC does not prove absence of
> DS";
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                               return sec;
> +                       } else if(sec == sec_status_insecure) {
> +                               /* this wasn't a delegation point. */
> +                               return sec;
> +                       } else if(sec == sec_status_secure) {
> +                               /* this proved no DS. */
> +                               *proof_ttl = ub_packed_rrset_ttl(nsec);
> +                               return sec;
> +                       }
>                 }
>                 /* if unchecked, fall through to next proof */
> +               /* For *.closest-encloser NSEC, there is a closer-match
> +                * check for the wildcard below. */
>         }
>
>         /* Otherwise, there is no NSEC at qname. This could be an ENT.
> @@ -252,7 +259,7 @@ val_nsec_prove_nodata_dsreply(struct mod
>                 if(rep->rrsets[i]->rk.type != htons(LDNS_RR_TYPE_NSEC))
>                         continue;
>                 if(!nsec_verify_rrset(env, ve, rep->rrsets[i], kkey,
> reason,
> -                       reason_bogus, qstate, reasonbuf, reasonlen)) {
> +                       reason_bogus, qstate, vq, reasonbuf, reasonlen)) {
>                         verbose(VERB_ALGO, "NSEC for empty non-terminal "
>                                 "did not verify.");
>                         *reason = "NSEC for empty non-terminal "
> Index: validator/val_nsec.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec.h,v
> diff -u -p -r1.6 val_nsec.h
> --- validator/val_nsec.h        4 Sep 2024 09:36:41 -0000       1.6
> +++ validator/val_nsec.h        20 Sep 2026 09:50:48 -0000
> @@ -52,6 +52,7 @@ struct ub_packed_rrset_key;
>  struct reply_info;
>  struct query_info;
>  struct key_entry_key;
> +struct val_qstate;
>
>  /**
>   * Check DS absence.
> @@ -68,6 +69,7 @@ struct key_entry_key;
>   * @param reason: string explaining why bogus.
>   * @param reason_bogus: relevant EDE code for validation failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
>   * @return security status.
> @@ -80,7 +82,8 @@ enum sec_status val_nsec_prove_nodata_ds
>         struct val_env* ve, struct query_info* qinfo,
>         struct reply_info* rep, struct key_entry_key* kkey,
>         time_t* proof_ttl, char** reason, sldns_ede_code* reason_bogus,
> -       struct module_qstate* qstate, char* reasonbuf, size_t reasonlen);
> +       struct module_qstate* qstate, struct val_qstate* vq, char*
> reasonbuf,
> +       size_t reasonlen);
>
>  /**
>   * nsec typemap check, takes an NSEC-type bitmap as argument, checks for
> type.
> Index: validator/val_nsec3.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec3.c,v
> diff -u -p -r1.11 val_nsec3.c
> --- validator/val_nsec3.c       26 May 2026 11:14:11 -0000      1.11
> +++ validator/val_nsec3.c       20 Sep 2026 09:50:48 -0000
> @@ -1248,6 +1248,10 @@ nsec3_prove_nameerror(struct module_env*
>         filter_init(&flt, list, num, qinfo); /* init RR iterator */
>         if(!flt.zone)
>                 return sec_status_bogus; /* no RRs */
> +       if(query_dname_compare(flt.zone, kkey->name) != 0) {
> +               verbose(VERB_ALGO, "NSEC3 name is not b32.signer name");
> +               return sec_status_bogus;
> +       }
>         if(!param_set_same(&flt, NULL))
>                 return sec_status_bogus; /* nsec3 params from distinct
> chains*/
>         if(nsec3_iteration_count_high(ve, &flt, kkey))
> @@ -1436,6 +1440,10 @@ nsec3_prove_nodata(struct module_env* en
>         filter_init(&flt, list, num, qinfo); /* init RR iterator */
>         if(!flt.zone)
>                 return sec_status_bogus; /* no RRs */
> +       if(query_dname_compare(flt.zone, kkey->name) != 0) {
> +               verbose(VERB_ALGO, "NSEC3 name is not b32.signer name");
> +               return sec_status_bogus;
> +       }
>         if(!param_set_same(&flt, NULL))
>                 return sec_status_bogus; /* nsec3 params from distinct
> chains*/
>         if(nsec3_iteration_count_high(ve, &flt, kkey))
> @@ -1461,6 +1469,10 @@ nsec3_prove_wildcard(struct module_env*
>         filter_init(&flt, list, num, qinfo); /* init RR iterator */
>         if(!flt.zone)
>                 return sec_status_bogus; /* no RRs */
> +       if(query_dname_compare(flt.zone, kkey->name) != 0) {
> +               verbose(VERB_ALGO, "NSEC3 name is not b32.signer name");
> +               return sec_status_bogus;
> +       }
>         if(!param_set_same(&flt, NULL))
>                 return sec_status_bogus; /* nsec3 params from distinct
> chains*/
>         if(nsec3_iteration_count_high(ve, &flt, kkey))
> @@ -1509,7 +1521,8 @@ static int
>  list_is_secure(struct module_env* env, struct val_env* ve,
>         struct ub_packed_rrset_key** list, size_t num,
>         struct key_entry_key* kkey, char** reason, sldns_ede_code
> *reason_bogus,
> -       struct module_qstate* qstate, char* reasonbuf, size_t reasonlen)
> +       struct module_qstate* qstate, struct val_qstate* vq, char*
> reasonbuf,
> +       size_t reasonlen)
>  {
>         struct packed_rrset_data* d;
>         size_t i;
> @@ -1525,7 +1538,7 @@ list_is_secure(struct module_env* env, s
>                         continue;
>                 d->security = val_verify_rrset_entry(env, ve, list[i],
> kkey,
>                         reason, reason_bogus, LDNS_SECTION_AUTHORITY,
> qstate,
> -                       &verified, reasonbuf, reasonlen);
> +                       vq, &verified, reasonbuf, reasonlen);
>                 if(d->security != sec_status_secure) {
>                         verbose(VERB_ALGO, "NSEC3 did not verify");
>                         return 0;
> @@ -1540,7 +1553,8 @@ nsec3_prove_nods(struct module_env* env,
>         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,
> -       struct nsec3_cache_table* ct, char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, struct nsec3_cache_table* ct, char*
> reasonbuf,
> +       size_t reasonlen)
>  {
>         struct nsec3_filter flt;
>         struct ce_response ce;
> @@ -1556,7 +1570,7 @@ nsec3_prove_nods(struct module_env* env,
>                 return sec_status_bogus; /* no valid NSEC3s, bogus */
>         }
>         if(!list_is_secure(env, ve, list, num, kkey, reason, reason_bogus,
> -               qstate, reasonbuf, reasonlen)) {
> +               qstate, vq, reasonbuf, reasonlen)) {
>                 *reason = "not all NSEC3 records secure";
>                 return sec_status_bogus; /* not all NSEC3 records secure */
>         }
> @@ -1565,6 +1579,11 @@ nsec3_prove_nods(struct module_env* env,
>                 *reason = "no NSEC3 records";
>                 return sec_status_bogus; /* no RRs */
>         }
> +       if(query_dname_compare(flt.zone, kkey->name) != 0) {
> +               verbose(VERB_ALGO, "NSEC3 name is not b32.signer name");
> +               *reason = "NSEC3 name is not b32.signer name";
> +               return sec_status_bogus;
> +       }
>         if(!param_set_same(&flt, reason))
>                 return sec_status_bogus; /* nsec3 params from distinct
> chains*/
>         if(nsec3_iteration_count_high(ve, &flt, kkey))
> @@ -1660,6 +1679,10 @@ nsec3_prove_nxornodata(struct module_env
>         filter_init(&flt, list, num, qinfo); /* init RR iterator */
>         if(!flt.zone)
>                 return sec_status_bogus; /* no RRs */
> +       if(query_dname_compare(flt.zone, kkey->name) != 0) {
> +               verbose(VERB_ALGO, "NSEC3 name is not b32.signer name");
> +               return sec_status_bogus;
> +       }
>         if(!param_set_same(&flt, NULL))
>                 return sec_status_bogus; /* nsec3 params from distinct
> chains*/
>         if(nsec3_iteration_count_high(ve, &flt, kkey))
> Index: validator/val_nsec3.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec3.h,v
> diff -u -p -r1.8 val_nsec3.h
> --- validator/val_nsec3.h       26 May 2026 11:14:11 -0000      1.8
> +++ validator/val_nsec3.h       20 Sep 2026 09:50:48 -0000
> @@ -78,6 +78,7 @@ struct reply_info;
>  struct query_info;
>  struct key_entry_key;
>  struct sldns_buffer;
> +struct val_qstate;
>
>  /**
>   *     0 1 2 3 4 5 6 7
> @@ -215,6 +216,7 @@ nsec3_prove_wildcard(struct module_env*
>   * @param reason: string for bogus result.
>   * @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate.
>   * @param ct: cached hashes table.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
> @@ -230,7 +232,8 @@ nsec3_prove_nods(struct module_env* env,
>         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,
> -       struct nsec3_cache_table* ct, char* reasonbuf, size_t reasonlen);
> +       struct val_qstate* vq, struct nsec3_cache_table* ct, char*
> reasonbuf,
> +       size_t reasonlen);
>
>  /**
>   * Prove NXDOMAIN or NODATA.
> Index: validator/val_secalgo.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_secalgo.c,v
> diff -u -p -r1.17 val_secalgo.c
> --- validator/val_secalgo.c     4 Sep 2024 09:36:41 -0000       1.17
> +++ validator/val_secalgo.c     20 Sep 2026 09:50:48 -0000
> @@ -745,11 +745,9 @@ verify_canonrrset(sldns_buffer* buf, int
>         if((algo == LDNS_DSA || algo == LDNS_DSA_NSEC3)
> &&(fake_dsa||fake_sha1))
>                 return sec_status_secure;
>  #endif
> -#ifndef USE_SHA1
>         if(fake_sha1 && (algo == LDNS_DSA || algo == LDNS_DSA_NSEC3 ||
> algo == LDNS_RSASHA1 || algo == LDNS_RSASHA1_NSEC3))
>                 return sec_status_secure;
> -#endif
> -
> +
>         if(!setup_key_digest(algo, &evp_key, &digest_type, key, keylen)) {
>                 verbose(VERB_QUERY, "verify: failed to setup key");
>                 *reason = "use of key for crypto failed";
> @@ -1874,9 +1872,9 @@ _verify_nettle_rsa(sldns_buffer* buf, un
>         }
>         mod_offset = exp_offset + exp_len;
>         nettle_rsa_public_key_init(&pubkey);
> -       pubkey.size = keylen - mod_offset;
>         nettle_mpz_set_str_256_u(pubkey.e, exp_len, &key[exp_offset]);
> -       nettle_mpz_set_str_256_u(pubkey.n, pubkey.size, &key[mod_offset]);
> +       nettle_mpz_set_str_256_u(pubkey.n, keylen - mod_offset,
> &key[mod_offset]);
> +       pubkey.size = nettle_mpz_sizeinbase_256_u(pubkey.n);
>
>         /* Digest content of "buf" and verify its RSA signature in
> "sigblock"*/
>         nettle_mpz_init_set_str_256_u(signature, sigblock_len,
> (uint8_t*)sigblock);
> Index: validator/val_sigcrypt.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_sigcrypt.c,v
> diff -u -p -r1.18 val_sigcrypt.c
> --- validator/val_sigcrypt.c    27 Jul 2026 14:14:39 -0000      1.18
> +++ validator/val_sigcrypt.c    20 Sep 2026 09:50:48 -0000
> @@ -82,6 +82,8 @@
>
>  /** Maximum number of RRSIG validations for an RRset. */
>  #define MAX_VALIDATE_RRSIGS 8
> +/** Maximum number of NSEC validations for a message. */
> +#define MAX_VALIDATE_NSECS 8
>
>  /** return number of rrs in an rrset */
>  static size_t
> @@ -305,6 +307,8 @@ ds_create_dnskey_digest(struct module_en
>          * digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA);
>          *      DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key.
> */
>         sldns_buffer_clear(b);
> +       if(!sldns_buffer_available(b, dnskey_rrset->rk.dname_len +
> dnskey_len-2))
> +               return 0; /* buffer too small */
>         sldns_buffer_write(b, dnskey_rrset->rk.dname,
>                 dnskey_rrset->rk.dname_len);
>         query_dname_tolower(sldns_buffer_begin(b));
> @@ -546,8 +550,10 @@ int algo_needs_missing(struct algo_needs
>   * @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 vq: validator qstate with attempt counts.
>   * @param numverified: incremented when the number of RRSIG validations
>   *     increases.
> + * @param num_tagmatches: incremented for tag matches.
>   * @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).
> @@ -559,7 +565,7 @@ dnskeyset_verify_rrset_sig(struct module
>         struct rbtree_type** sortree,
>         char** reason, sldns_ede_code *reason_bogus,
>         sldns_pkt_section section, struct module_qstate* qstate,
> -       int* numverified)
> +       struct val_qstate* vq, int* numverified, size_t* num_tagmatches)
>  {
>         /* find matching keys and check them */
>         enum sec_status sec = sec_status_bogus;
> @@ -578,6 +584,14 @@ dnskeyset_verify_rrset_sig(struct module
>         }
>
>         for(i=0; i<num; i++) {
> +               if((*num_tagmatches)++ > MAX_TAG_MATCHES) {
> +                       *reason = "too many tag matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "verify sig: too many tag
> matches, "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       return sec_status_bogus;
> +               }
>                 /* see if key matches keytag and algo */
>                 if(algo != dnskey_get_algo(dnskey, i) ||
>                         tag != dnskey_calc_keytag(dnskey, i))
> @@ -585,6 +599,26 @@ dnskeyset_verify_rrset_sig(struct module
>                 numchecked ++;
>                 (*numverified)++;
>
> +               if(vq && vq->num_validation_attempts++ >
> env->cfg->val_validation_attempts) {
> +                       *reason = "too many validation attempts";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "verify sig: too many
> validation attempts, "
> +                               "val-validation-attempts (%d); bogus",
> env->cfg->val_validation_attempts);
> +                       return sec_status_bogus;
> +               }
> +               if(vq && (ntohs(rrset->rk.type) == LDNS_RR_TYPE_NSEC ||
> +                       ntohs(rrset->rk.type) == LDNS_RR_TYPE_NSEC3) &&
> +                       vq->num_nsec_attempts++ > MAX_VALIDATE_NSECS) {
> +                       *reason = "too many NSEC or NSEC3 validation
> attempts";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "verify sig: too many NSEC or
> NSEC3 validation attempts, "
> +                               "(%d); bogus", MAX_VALIDATE_NSECS);
> +                       vq->num_nsec_attempts_exceeded = 1;
> +                       return sec_status_bogus;
> +               }
> +
>                 /* see if key verifies */
>                 sec = dnskey_verify_rrset_sig(env->scratch,
>                         env->scratch_buffer, ve, now, rrset, dnskey, i,
> @@ -624,11 +658,12 @@ 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, int*
> verified,
> -       char* reasonbuf, size_t reasonlen)
> +       sldns_pkt_section section, struct module_qstate* qstate,
> +       struct val_qstate* vq, int* verified, char* reasonbuf,
> +       size_t reasonlen)
>  {
>         enum sec_status sec;
> -       size_t i, num;
> +       size_t i, num, num_tagmatches = 0;
>         rbtree_type* sortree = NULL;
>         /* make sure that for all DNSKEY algorithms there are valid sigs */
>         struct algo_needs needs;
> @@ -656,9 +691,19 @@ dnskeyset_verify_rrset(struct module_env
>                 }
>         }
>         for(i=0; i<num; i++) {
> +               if(num_tagmatches > MAX_TAG_MATCHES) {
> +                       *reason = "too many tag matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "rrset failed to verify: too
> many tag matches, "
> +                               "MAX_TAG_MATCHES (%d)", MAX_TAG_MATCHES);
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       return sec_status_bogus;
> +               }
>                 sec = dnskeyset_verify_rrset_sig(env, ve, *env->now,
> rrset,
>                         dnskey, i, &sortree, reason, reason_bogus,
> -                       section, qstate, verified);
> +                       section, qstate, vq, verified, &num_tagmatches);
>                 /* see which algorithm has been fixed up */
>                 if(sec == sec_status_secure) {
>                         if(!sigalg)
> @@ -707,7 +752,8 @@ enum sec_status
>  dnskey_verify_rrset(struct module_env* env, struct val_env* ve,
>          struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key*
> dnskey,
>         size_t dnskey_idx, char** reason, sldns_ede_code *reason_bogus,
> -       sldns_pkt_section section, struct module_qstate* qstate)
> +       sldns_pkt_section section, struct module_qstate* qstate,
> +       struct val_qstate* vq, size_t* num_tagmatches)
>  {
>         enum sec_status sec;
>         size_t i, num, numchecked = 0, numindeterminate = 0;
> @@ -728,9 +774,26 @@ dnskey_verify_rrset(struct module_env* e
>         }
>         for(i=0; i<num; i++) {
>                 /* see if sig matches keytag and algo */
> +               if((*num_tagmatches)++ > MAX_TAG_MATCHES) {
> +                       *reason = "too many tag matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "rrset failed to verify: too
> many tag matches, "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       return sec_status_bogus;
> +               }
>                 if(algo != rrset_get_sig_algo(rrset, i) ||
>                         tag != rrset_get_sig_keytag(rrset, i))
>                         continue;
> +               if(vq && vq->num_validation_attempts++ >
> env->cfg->val_validation_attempts) {
> +                       *reason = "too many validation attempts";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "rrset failed to verify: too
> many validation attempts, "
> +                               "val-validation-attempts (%d); bogus",
> env->cfg->val_validation_attempts);
> +                       return sec_status_bogus;
> +               }
> +
>                 buf_canon = 0;
>                 sec = dnskey_verify_rrset_sig(env->scratch,
>                         env->scratch_buffer, ve, *env->now, rrset,
> @@ -1083,6 +1146,18 @@ insert_can_owner(sldns_buffer* buf, stru
>         }
>  }
>
> +/** lowercase a wire dname but never step past end */
> +static void
> +canon_dname_tolower(uint8_t* d, uint8_t* end)
> +{
> +       uint8_t lab;
> +       while(d < end && (lab = *d) != 0) {
> +               if((size_t)lab+1 > (size_t)(end-d)) return; /* malformed */
> +               for(d++; lab; lab--, d++)
> +                       *d = (uint8_t)tolower((unsigned char)*d);
> +       }
> +}
> +
>  /**
>   * Canonicalize Rdata in buffer.
>   * @param buf: buffer at position just after the rdata.
> @@ -1094,6 +1169,7 @@ canonicalize_rdata(sldns_buffer* buf, st
>         size_t len)
>  {
>         uint8_t* datstart = sldns_buffer_current(buf)-len+2;
> +       uint8_t* datend = sldns_buffer_current(buf);
>         size_t firstlen;
>         switch(ntohs(rrset->rk.type)) {
>                 case LDNS_RR_TYPE_NXT:
> @@ -1107,16 +1183,16 @@ canonicalize_rdata(sldns_buffer* buf, st
>                 case LDNS_RR_TYPE_PTR:
>                 case LDNS_RR_TYPE_DNAME:
>                         /* type only has a single argument, the name */
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         return;
>                 case LDNS_RR_TYPE_MINFO:
>                 case LDNS_RR_TYPE_RP:
>                 case LDNS_RR_TYPE_SOA:
>                         /* two names after another */
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         firstlen = dname_valid(datstart, len-2);
>                         if(firstlen && firstlen < len-2)
> -                               query_dname_tolower(datstart + firstlen);
> +                               canon_dname_tolower(datstart + firstlen,
> datend);
>                         return;
>                 case LDNS_RR_TYPE_RT:
>                 case LDNS_RR_TYPE_AFSDB:
> @@ -1126,7 +1202,7 @@ canonicalize_rdata(sldns_buffer* buf, st
>                         if(len < 2+2+1) /* rdlen, skiplen, 1byteroot */
>                                 return;
>                         datstart += 2;
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         return;
>                 case LDNS_RR_TYPE_SIG:
>                 /* downcase the RRSIG, compat with BIND (kept it from SIG)
> */
> @@ -1135,17 +1211,17 @@ canonicalize_rdata(sldns_buffer* buf, st
>                         if(len < 2+18+1)
>                                 return;
>                         datstart += 18;
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         return;
>                 case LDNS_RR_TYPE_PX:
>                         /* skip, then two names after another */
>                         if(len < 2+2+1)
>                                 return;
>                         datstart += 2;
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         firstlen = dname_valid(datstart, len-2-2);
>                         if(firstlen && firstlen < len-2-2)
> -                               query_dname_tolower(datstart + firstlen);
> +                               canon_dname_tolower(datstart + firstlen,
> datend);
>                         return;
>                 case LDNS_RR_TYPE_NAPTR:
>                         if(len < 2+4)
> @@ -1166,14 +1242,14 @@ canonicalize_rdata(sldns_buffer* buf, st
>                         datstart += (size_t)datstart[0]+1;
>                         if(len < 1)     /* check name is at least 1 byte*/
>                                 return;
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         return;
>                 case LDNS_RR_TYPE_SRV:
>                         /* skip fixed part */
>                         if(len < 2+6+1)
>                                 return;
>                         datstart += 6;
> -                       query_dname_tolower(datstart);
> +                       canon_dname_tolower(datstart, datend);
>                         return;
>
>                 /* do not canonicalize NSEC rdata name, compat with
> @@ -1295,14 +1371,32 @@ rrset_canonical(struct regional* region,
>         }
>
>         sldns_buffer_clear(buf);
> +       if(sldns_buffer_remaining(buf) < siglen || siglen < 18+1) {
> +               verbose(VERB_ALGO, "verify: failed to canonicalize, "
> +                       "rrset too big");
> +               return 0;
> +       }
>         sldns_buffer_write(buf, sig, siglen);
>         /* canonicalize signer name */
> -       query_dname_tolower(sldns_buffer_begin(buf)+18);
> +       canon_dname_tolower(sldns_buffer_begin(buf)+18,
> +               sldns_buffer_current(buf));
> +
> +       if(sldns_buffer_remaining(buf) < k->rk.dname_len+2) {
> +               /* Check if the first can_owner name can fit in the buffer.
> +                * The length is k->rk.dname_len or k->rk.dname_len+2
> +                * if it has '*.' in prefixed. Checks the upper bound,
> +                * also realistically the rest of the rrtype, rrclass,
> origttl,
> +                * rdata and so on has to be inserted, so that extra space
> has
> +                * to be there. */
> +               verbose(VERB_ALGO, "verify: failed to canonicalize, "
> +                       "rrset too big");
> +               return 0;
> +       }
>         RBTREE_FOR(walk, struct canon_rr*, (*sortree)) {
>                 /* see if there is enough space left in the buffer */
>                 if(sldns_buffer_remaining(buf) < can_owner_len + 2 + 2 + 4
>                         + d->rr_len[walk->rr_idx]) {
> -                       log_err("verify: failed to canonicalize, "
> +                       verbose(VERB_ALGO, "verify: failed to
> canonicalize, "
>                                 "rrset too big");
>                         return 0;
>                 }
> @@ -1311,6 +1405,13 @@ rrset_canonical(struct regional* region,
>                         sldns_buffer_write(buf, can_owner, can_owner_len);
>                 else    insert_can_owner(buf, k, sig, &can_owner,
>                                 &can_owner_len);
> +               /* Check again, if the rdata can fit in the buffer */
> +               if(sldns_buffer_remaining(buf) < 2 + 2 + 4
> +                        + d->rr_len[walk->rr_idx]) {
> +                       verbose(VERB_ALGO, "verify: failed to
> canonicalize, "
> +                               "rrset too big");
> +                        return 0;
> +                }
>                 sldns_buffer_write(buf, &k->rk.type, 2);
>                 sldns_buffer_write(buf, &k->rk.rrset_class, 2);
>                 sldns_buffer_write(buf, sig+4, 4);
> @@ -1325,10 +1426,11 @@ rrset_canonical(struct regional* region,
>          * the non-existence proves. */
>         if(ntohs(k->rk.type) == LDNS_RR_TYPE_NSEC &&
>                 section == LDNS_SECTION_AUTHORITY && qstate) {
> -               k->rk.dname = regional_alloc_init(qstate->region,
> can_owner,
> +               uint8_t* new_dname = regional_alloc_init(qstate->region,
> can_owner,
>                         can_owner_len);
> -               if(!k->rk.dname)
> +               if(!new_dname)
>                         return 0;
> +               k->rk.dname = new_dname;
>                 k->rk.dname_len = can_owner_len;
>         }
>
> @@ -1361,11 +1463,17 @@ rrset_canonicalize_to_buffer(struct regi
>         canonical_sort(k, d, sortree, rrs);
>
>         sldns_buffer_clear(buf);
> +       if(sldns_buffer_remaining(buf) < k->rk.dname_len) {
> +               /* Check if the first can_owner name can fit in the
> buffer. */
> +               verbose(VERB_ALGO, "verify: failed to canonicalize, "
> +                       "rrset too big");
> +               return 0;
> +       }
>         RBTREE_FOR(walk, struct canon_rr*, sortree) {
>                 /* see if there is enough space left in the buffer */
>                 if(sldns_buffer_remaining(buf) < can_owner_len + 2 + 2 + 4
>                         + d->rr_len[walk->rr_idx]) {
> -                       log_err("verify: failed to canonicalize, "
> +                       verbose(VERB_ALGO, "verify: failed to
> canonicalize, "
>                                 "rrset too big");
>                         return 0;
>                 }
> @@ -1378,6 +1486,13 @@ rrset_canonicalize_to_buffer(struct regi
>                         query_dname_tolower(can_owner);
>                         can_owner_len = k->rk.dname_len;
>                 }
> +               /* Check again, if the rdata can fit in the buffer */
> +               if(sldns_buffer_remaining(buf) < 2 + 2 + 4
> +                        + d->rr_len[walk->rr_idx]) {
> +                       verbose(VERB_ALGO, "verify: failed to
> canonicalize, "
> +                               "rrset too big");
> +                        return 0;
> +                }
>                 sldns_buffer_write(buf, &k->rk.type, 2);
>                 sldns_buffer_write(buf, &k->rk.rrset_class, 2);
>                 sldns_buffer_write_u32(buf, d->rr_ttl[walk->rr_idx]);
> @@ -1612,6 +1727,30 @@ dnskey_verify_rrset_sig(struct regional*
>                         *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
>                 return sec_status_bogus; /* signer name offtree */
>         }
> +       /* NSEC3, the owner name must be the <base32hash>.signername */
> +       if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_NSEC3 &&
> +               rrset->rk.dname_len > 0) {
> +               uint8_t* dnameless = rrset->rk.dname;
> +               size_t dnamelesslen = rrset->rk.dname_len;
> +               dname_remove_label(&dnameless, &dnamelesslen);
> +               if(query_dname_compare(dnameless, signer) != 0) {
> +                       verbose(VERB_QUERY, "verify: NSEC3 owner name is
> not b32.signer name");
> +                       *reason = "NSEC3 owner name is not b32.signer
> name";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       return sec_status_bogus; /* NSEC3 owner not
> b32.signer */
> +               }
> +       }
> +       /* NSEC, a next owner that is not under the signer is not
> allowed.*/
> +       if(ntohs(rrset->rk.type) == LDNS_RR_TYPE_NSEC &&
> +               !nsec_nextowner_subdomain(rrset, signer)) {
> +               verbose(VERB_QUERY, "verify: NSEC next owner overreaches
> signer name");
> +               *reason = "NSEC next owner overreaches signer name";
> +               if(reason_bogus)
> +                       *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +               return sec_status_bogus; /* nextowner overreaching */
> +       }
> +
>         sigblock = (unsigned char*)signer+signer_len;
>         if(siglen < 2+18+signer_len+1) {
>                 verbose(VERB_QUERY, "verify: too short, no signature
> data");
> Index: validator/val_sigcrypt.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_sigcrypt.h,v
> diff -u -p -r1.8 val_sigcrypt.h
> --- validator/val_sigcrypt.h    26 May 2026 11:14:11 -0000      1.8
> +++ validator/val_sigcrypt.h    20 Sep 2026 09:50:48 -0000
> @@ -53,6 +53,7 @@ struct ub_packed_rrset_key;
>  struct rbtree_type;
>  struct regional;
>  struct sldns_buffer;
> +struct val_qstate;
>
>  /** number of entries in algorithm needs array */
>  #define ALGO_NEEDS_MAX 256
> @@ -262,6 +263,7 @@ uint16_t dnskey_get_flags(struct ub_pack
>   * @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 vq: validator qstate with attempt counts.
>   * @param verified: if not NULL the number of RRSIG validations is
> returned.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
> @@ -273,8 +275,9 @@ enum sec_status dnskeyset_verify_rrset(s
>         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, int*
> verified,
> -       char* reasonbuf, size_t reasonlen);
> +       sldns_pkt_section section, struct module_qstate* qstate,
> +       struct val_qstate* vq, int* verified, char* reasonbuf,
> +       size_t reasonlen);
>
>  /**
>   * verify rrset against one specific dnskey (from rrset)
> @@ -287,13 +290,16 @@ enum sec_status dnskeyset_verify_rrset(s
>   * @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 vq: validator qstate with attempt counts.
> + * @param num_tagmatches: incremented to keep track of tag matches.
>   * @return secure if *this* key signs any of the signatures on rrset.
>   *     unchecked on error or and bogus on bad signature.
>   */
>  enum sec_status dnskey_verify_rrset(struct module_env* env, struct
> val_env* ve,
>          struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key*
> dnskey,
>         size_t dnskey_idx, char** reason, sldns_ede_code *reason_bogus,
> -       sldns_pkt_section section, struct module_qstate* qstate);
> +       sldns_pkt_section section, struct module_qstate* qstate,
> +       struct val_qstate* vq, size_t* num_tagmatches);
>
>  /**
>   * verify rrset, with specific dnskey(from set), for a specific rrsig
> Index: validator/val_utils.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_utils.c,v
> diff -u -p -r1.19 val_utils.c
> --- validator/val_utils.c       27 Jul 2026 14:14:39 -0000      1.19
> +++ validator/val_utils.c       20 Sep 2026 09:50:48 -0000
> @@ -406,7 +406,8 @@ val_verify_rrset(struct module_env* env,
>          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,
> -       int *verified, char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, int *verified, char* reasonbuf,
> +       size_t reasonlen)
>  {
>         enum sec_status sec;
>         struct packed_rrset_data* d = (struct packed_rrset_data*)rrset->
> @@ -431,7 +432,8 @@ val_verify_rrset(struct module_env* env,
>         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, verified, reasonbuf,
> reasonlen);
> +               reason_bogus, section, qstate, vq, verified, reasonbuf,
> +               reasonlen);
>         verbose(VERB_ALGO, "verify result: %s", sec_status_to_string(sec));
>         regional_free_all(env->scratch);
>
> @@ -475,7 +477,8 @@ val_verify_rrset_entry(struct module_env
>          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,
> -       int* verified, char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, int* verified, char* reasonbuf,
> +       size_t reasonlen)
>  {
>         /* temporary dnskey rrset-key */
>         struct ub_packed_rrset_key dnskey;
> @@ -489,7 +492,8 @@ val_verify_rrset_entry(struct module_env
>         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, verified, reasonbuf,
> reasonlen);
> +               reason_bogus, section, qstate, vq, verified, reasonbuf,
> +               reasonlen);
>         return sec;
>  }
>
> @@ -499,13 +503,20 @@ verify_dnskeys_with_ds_rr(struct module_
>         struct ub_packed_rrset_key* dnskey_rrset,
>          struct ub_packed_rrset_key* ds_rrset, size_t ds_idx, char**
> reason,
>         sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -       int *nonechecked, char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, int *nonechecked, char* reasonbuf,
> +       size_t reasonlen, size_t* num_tagmatches,
> +       size_t* num_tagmatches_dnskeysig)
>  {
>         enum sec_status sec = sec_status_bogus;
>         size_t i, num, numchecked = 0, numhashok = 0, numsizesupp = 0;
>         num = rrset_get_count(dnskey_rrset);
>         *nonechecked = 0;
>         for(i=0; i<num; i++) {
> +               if((*num_tagmatches)++ > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "DS match attempt reached "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       return sec_status_bogus;
> +               }
>                 /* Skip DNSKEYs that don't match the basic criteria. */
>                 if(ds_get_key_algo(ds_rrset, ds_idx)
>                    != dnskey_get_algo(dnskey_rrset, i)
> @@ -518,6 +529,15 @@ verify_dnskeys_with_ds_rr(struct module_
>                         ds_get_key_algo(ds_rrset, ds_idx),
>                         ds_get_keytag(ds_rrset, ds_idx));
>
> +               if(vq && vq->num_hash_attempts++ >
> env->cfg->val_hash_attempts) {
> +                       *reason = "too many hash attempts";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       verbose(VERB_ALGO, "rrset failed to verify: too
> many hash attempts, "
> +                               "val-hash-attempts (%d); bogus",
> env->cfg->val_hash_attempts);
> +                       return sec_status_bogus;
> +               }
> +
>                 /* Convert the candidate DNSKEY into a hash using the
>                  * same DS hash algorithm. */
>                 if(!ds_digest_match_dnskey(env, dnskey_rrset, i, ds_rrset,
> @@ -541,8 +561,14 @@ verify_dnskeys_with_ds_rr(struct module_
>
>                 /* Otherwise, we have a match! Make sure that the DNSKEY
>                  * verifies *with this key*  */
> +               if(*num_tagmatches_dnskeysig > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "DS that matched has too many
> DNSKEY to RRSIG tag matches "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       return sec_status_bogus;
> +               }
>                 sec = dnskey_verify_rrset(env, ve, dnskey_rrset,
> dnskey_rrset,
> -                       i, reason, reason_bogus, LDNS_SECTION_ANSWER,
> qstate);
> +                       i, reason, reason_bogus, LDNS_SECTION_ANSWER,
> qstate,
> +                       vq, num_tagmatches_dnskeysig);
>                 if(sec == sec_status_secure) {
>                         return sec;
>                 }
> @@ -586,14 +612,14 @@ val_verify_DNSKEY_with_DS(struct module_
>         struct ub_packed_rrset_key* dnskey_rrset,
>         struct ub_packed_rrset_key* ds_rrset, uint8_t* sigalg, char**
> reason,
>         sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -       char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, char* reasonbuf, size_t reasonlen)
>  {
>         /* as long as this is false, we can consider this DS rrset to be
>          * equivalent to no DS rrset. */
>         int has_useful_ds = 0, digest_algo, alg, has_algo_refusal = 0,
>                 nonechecked, has_checked_ds = 0;
>         struct algo_needs needs;
> -       size_t i, num;
> +       size_t i, num, num_tagmatches = 0, num_tagmatches_dnskeysig = 0;
>         enum sec_status sec;
>
>         if(dnskey_rrset->rk.dname_len != ds_rrset->rk.dname_len ||
> @@ -615,6 +641,13 @@ val_verify_DNSKEY_with_DS(struct module_
>         }
>         num = rrset_get_count(ds_rrset);
>         for(i=0; i<num; i++) {
> +               if(num_tagmatches > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "DS verify attempt reached "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       *reason = "DS verify has too many tag matches";
> +                       return sec_status_bogus;
> +               }
> +
>                 /* Check to see if we can understand this DS.
>                  * And check it is the strongest digest */
>                 if(!ds_digest_algo_is_supported(ds_rrset, i) ||
> @@ -623,9 +656,16 @@ val_verify_DNSKEY_with_DS(struct module_
>                         continue;
>                 }
>
> +               if(num_tagmatches_dnskeysig > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "DS verify attempt reached "
> +                               "DNSKEY to RRSIG MAX_TAG_MATCHES (%d);
> bogus", MAX_TAG_MATCHES);
> +                       *reason = "DS verify has too many DNSKEY to RRSIG
> tag matches";
> +                       return sec_status_bogus;
> +               }
>                 sec = verify_dnskeys_with_ds_rr(env, ve, dnskey_rrset,
> -                       ds_rrset, i, reason, reason_bogus, qstate,
> -                       &nonechecked, reasonbuf, reasonlen);
> +                       ds_rrset, i, reason, reason_bogus, qstate, vq,
> +                       &nonechecked, reasonbuf, reasonlen,
> &num_tagmatches,
> +                       &num_tagmatches_dnskeysig);
>                 if(sec == sec_status_insecure) {
>                         /* DNSKEY too large unsupported or algo refused by
>                          * crypto lib. */
> @@ -687,12 +727,12 @@ val_verify_new_DNSKEYs(struct regional*
>         struct val_env* ve, struct ub_packed_rrset_key* dnskey_rrset,
>         struct ub_packed_rrset_key* ds_rrset, int downprot, char** reason,
>         sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -       char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, char* reasonbuf, size_t reasonlen)
>  {
>         uint8_t sigalg[ALGO_NEEDS_MAX+1];
>         enum sec_status sec = val_verify_DNSKEY_with_DS(env, ve,
>                 dnskey_rrset, ds_rrset, downprot?sigalg:NULL, reason,
> -               reason_bogus, qstate, reasonbuf, reasonlen);
> +               reason_bogus, qstate, vq, reasonbuf, reasonlen);
>
>         if(sec == sec_status_secure) {
>                 return key_entry_create_rrset(region,
> @@ -718,14 +758,14 @@ val_verify_DNSKEY_with_TA(struct module_
>         struct ub_packed_rrset_key* ta_ds,
>         struct ub_packed_rrset_key* ta_dnskey, uint8_t* sigalg, char**
> reason,
>         sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -       char* reasonbuf, size_t reasonlen)
> +       struct val_qstate* vq, char* reasonbuf, size_t reasonlen)
>  {
>         /* as long as this is false, we can consider this anchor to be
>          * equivalent to no anchor. */
>         int has_useful_ta = 0, digest_algo = 0, alg, has_algo_refusal = 0,
>                 nonechecked, has_checked_ds = 0;
>         struct algo_needs needs;
> -       size_t i, num;
> +       size_t i, num, num_tagmatches = 0, num_tagmatches_dnskeysig = 0;
>         enum sec_status sec;
>
>         if(ta_ds && (dnskey_rrset->rk.dname_len != ta_ds->rk.dname_len ||
> @@ -761,6 +801,15 @@ val_verify_DNSKEY_with_TA(struct module_
>         if(ta_ds) {
>             num = rrset_get_count(ta_ds);
>             for(i=0; i<num; i++) {
> +               if(num_tagmatches > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "anchor DS verify attempt
> reached "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       *reason = "anchor DS verify has too many tag
> matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       return sec_status_bogus;
> +               }
> +
>                 /* Check to see if we can understand this DS.
>                  * And check it is the strongest digest */
>                 if(!ds_digest_algo_is_supported(ta_ds, i) ||
> @@ -768,9 +817,18 @@ val_verify_DNSKEY_with_TA(struct module_
>                         ds_get_digest_algo(ta_ds, i) != digest_algo)
>                         continue;
>
> +               if(num_tagmatches_dnskeysig > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "anchor DS verify has too many
> DNSKEY to RRSIG tag matches "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       *reason = "anchor DS verify has too many DNSKEY to
> RRSIG tag matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       return sec_status_bogus;
> +               }
>                 sec = verify_dnskeys_with_ds_rr(env, ve, dnskey_rrset,
> -                       ta_ds, i, reason, reason_bogus, qstate,
> &nonechecked,
> -                       reasonbuf, reasonlen);
> +                       ta_ds, i, reason, reason_bogus, qstate, vq,
> +                       &nonechecked, reasonbuf, reasonlen,
> &num_tagmatches,
> +                       &num_tagmatches_dnskeysig);
>                 if(sec == sec_status_insecure) {
>                         has_algo_refusal = 1;
>                         continue;
> @@ -813,8 +871,16 @@ val_verify_DNSKEY_with_TA(struct module_
>                 /* we saw a useful TA */
>                 has_useful_ta = 1;
>
> +               if(num_tagmatches_dnskeysig > MAX_TAG_MATCHES) {
> +                       verbose(VERB_ALGO, "anchor DS that matched has too
> many DNSKEY to RRSIG tag matches "
> +                               "MAX_TAG_MATCHES (%d); bogus",
> MAX_TAG_MATCHES);
> +                       *reason = "anchor DS that matched has too many
> DNSKEY to RRSIG tag matches";
> +                       if(reason_bogus)
> +                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
> +                       return sec_status_bogus;
> +               }
>                 sec = dnskey_verify_rrset(env, ve, dnskey_rrset,
> -                       ta_dnskey, i, reason, reason_bogus,
> LDNS_SECTION_ANSWER, qstate);
> +                       ta_dnskey, i, reason, reason_bogus,
> LDNS_SECTION_ANSWER, qstate, vq, &num_tagmatches_dnskeysig);
>                 if(sec == sec_status_secure) {
>                         if(!sigalg || algo_needs_set_secure(&needs,
>                                 (uint8_t)dnskey_get_algo(ta_dnskey, i))) {
> @@ -862,12 +928,13 @@ val_verify_new_DNSKEYs_with_ta(struct re
>         struct ub_packed_rrset_key* ta_ds_rrset,
>         struct ub_packed_rrset_key* ta_dnskey_rrset, int downprot,
>         char** reason, sldns_ede_code *reason_bogus,
> -       struct module_qstate* qstate, char* reasonbuf, size_t reasonlen)
> +       struct module_qstate* qstate, struct val_qstate* vq, char*
> reasonbuf,
> +       size_t reasonlen)
>  {
>         uint8_t sigalg[ALGO_NEEDS_MAX+1];
>         enum sec_status sec = val_verify_DNSKEY_with_TA(env, ve,
>                 dnskey_rrset, ta_ds_rrset, ta_dnskey_rrset,
> -               downprot?sigalg:NULL, reason, reason_bogus, qstate,
> +               downprot?sigalg:NULL, reason, reason_bogus, qstate, vq,
>                 reasonbuf, reasonlen);
>
>         if(sec == sec_status_secure) {
> @@ -1052,7 +1119,7 @@ val_fill_reply(struct reply_info* chase,
>                         chase->rrsets[chase->an_numrrsets++] =
> orig->rrsets[j];
>                         chase->rrsets[chase->an_numrrsets++] =
> orig->rrsets[i];
>                 }
> -       }
> +       }
>         /* AUTHORITY section */
>         for(i = (skip > orig->an_numrrsets)?skip:orig->an_numrrsets;
>                 i<orig->an_numrrsets+orig->ns_numrrsets;
> @@ -1086,6 +1153,23 @@ val_fill_reply(struct reply_info* chase,
>                 chase->ar_numrrsets;
>  }
>
> +void val_reply_remove_answers(struct reply_info* rep, size_t index,
> +       size_t count)
> +{
> +       log_assert(index < rep->rrset_count);
> +       log_assert(index < rep->an_numrrsets);
> +       if(count == 0)
> +               return; /* nothing to do */
> +       log_assert(index+(count-1) < rep->rrset_count);
> +       log_assert(index+(count-1) < rep->an_numrrsets);
> +       if(rep->rrset_count - (count-1) - index - 1 > 0)
> +         memmove(rep->rrsets+index, rep->rrsets+index+(count-1)+1,
> +               sizeof(struct ub_packed_rrset_key*)*
> +               (rep->rrset_count - (count-1) - index - 1));
> +       rep->an_numrrsets -= count;
> +       rep->rrset_count -= count;
> +}
> +
>  void val_reply_remove_auth(struct reply_info* rep, size_t index)
>  {
>         log_assert(index < rep->rrset_count);
> @@ -1319,10 +1403,11 @@ val_find_DS(struct module_env* env, uint
>                 /* DS rrset exists. Return it to the validator
> immediately*/
>                 struct ub_packed_rrset_key* copy =
> packed_rrset_copy_region(
>                         rrset, region, *env->now);
> -               struct packed_rrset_data* d = copy->entry.data;
> +               struct packed_rrset_data* d;
>                 lock_rw_unlock(&rrset->entry.lock);
>                 if(!copy)
>                         return NULL;
> +               d = (struct packed_rrset_data*)copy->entry.data;
>                 msg = dns_msg_create(nm, nmlen, LDNS_RR_TYPE_DS, c,
> region, 1);
>                 if(!msg)
>                         return NULL;
> @@ -1365,4 +1450,21 @@ int derive_cname_from_dname(struct ub_pa
>         memmove(out, cname->rk.dname, prefix_len);
>         memmove(out+prefix_len, dname_target, dname_target_len);
>         return 1;
> +}
> +
> +int nsec_nextowner_subdomain(struct ub_packed_rrset_key* rrset, uint8_t*
> name)
> +{
> +       struct packed_rrset_data* d;
> +       uint8_t* next;
> +       size_t nextlen;
> +       if(ntohs(rrset->rk.type) != LDNS_RR_TYPE_NSEC)
> +               return 0;
> +       d = (struct packed_rrset_data*)rrset->entry.data;
> +       if(!d || d->count == 0)
> +               return 0;
> +       next = d->rr_data[0]+2;
> +       nextlen = dname_valid(next, d->rr_len[0]-2);
> +       if(nextlen == 0)
> +               return 0; /* malformed */
> +       return dname_subdomain_c(next, name);
>  }
> Index: validator/val_utils.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/val_utils.h,v
> diff -u -p -r1.11 val_utils.h
> --- validator/val_utils.h       27 Jul 2026 14:14:39 -0000      1.11
> +++ validator/val_utils.h       20 Sep 2026 09:50:48 -0000
> @@ -55,6 +55,11 @@ struct regional;
>  struct val_anchors;
>  struct rrset_cache;
>  struct sock_list;
> +struct val_qstate;
> +
> +/** Maximum number of matches with key tag and algorithm, for DNSKEY to
> + * RRSIG and DS to DNSKEY. Since the number is O(N*N), there is a limit.
> */
> +#define MAX_TAG_MATCHES 256
>
>  /**
>   * Response classifications for the validator. The different types of
> proofs.
> @@ -124,6 +129,7 @@ void val_find_signer(enum val_classifica
>   * @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 vq: validator qstate with attempt counts.
>   * @param verified: if not NULL, the number of RRSIG validations is
> returned.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
> @@ -133,7 +139,8 @@ enum sec_status val_verify_rrset_entry(s
>         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,
> -       int* verified, char* reasonbuf, size_t reasonlen);
> +       struct val_qstate* vq, int* verified, char* reasonbuf,
> +       size_t reasonlen);
>
>  /**
>   * Verify DNSKEYs with DS rrset. Like val_verify_new_DNSKEYs but
> @@ -148,6 +155,7 @@ enum sec_status val_verify_rrset_entry(s
>   * @param reason: reason of failure. Fixed string or alloced in scratch.
>   * @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate with attempt counts.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
>   * @return: sec_status_secure if a DS matches.
> @@ -158,7 +166,7 @@ enum sec_status val_verify_DNSKEY_with_D
>      struct val_env* ve, struct ub_packed_rrset_key* dnskey_rrset,
>      struct ub_packed_rrset_key* ds_rrset, uint8_t* sigalg, char** reason,
>      sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -    char* reasonbuf, size_t reasonlen);
> +    struct val_qstate* vq, char* reasonbuf, size_t reasonlen);
>
>  /**
>   * Verify DNSKEYs with DS and DNSKEY rrset.  Like
> val_verify_DNSKEY_with_DS
> @@ -172,8 +180,9 @@ enum sec_status val_verify_DNSKEY_with_D
>   *   algorithm is enough.  The list of signalled algorithms is returned,
>   *   must have enough space for ALGO_NEEDS_MAX+1.
>   * @param reason: reason of failure. Fixed string or alloced in scratch.
> -* @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
> + * @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate with attempt counts.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
>   * @return: sec_status_secure if a DS matches.
> @@ -185,7 +194,7 @@ enum sec_status val_verify_DNSKEY_with_T
>      struct ub_packed_rrset_key* ta_ds,
>      struct ub_packed_rrset_key* ta_dnskey, uint8_t* sigalg, char** reason,
>      sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -    char* reasonbuf, size_t reasonlen);
> +    struct val_qstate* vq, char* reasonbuf, size_t reasonlen);
>
>  /**
>   * Verify new DNSKEYs with DS rrset. The DS contains hash values that
> should
> @@ -202,6 +211,7 @@ enum sec_status val_verify_DNSKEY_with_T
>   * @param reason: reason of failure. Fixed string or alloced in scratch.
>   * @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate with attempt counts.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
>   * @return a KeyEntry. This will either contain the now trusted
> @@ -219,7 +229,7 @@ struct key_entry_key* val_verify_new_DNS
>      struct ub_packed_rrset_key* dnskey_rrset,
>      struct ub_packed_rrset_key* ds_rrset, int downprot, char** reason,
>      sldns_ede_code *reason_bogus, struct module_qstate* qstate,
> -    char* reasonbuf, size_t reasonlen);
> +    struct val_qstate* vq, char* reasonbuf, size_t reasonlen);
>
>  /**
>   * Verify rrset with trust anchor: DS and DNSKEY rrset.
> @@ -235,6 +245,7 @@ struct key_entry_key* val_verify_new_DNS
>   * @param reason: reason of failure. Fixed string or alloced in scratch.
>   * @param reason_bogus: EDE (RFC8914) code paired with the reason of
> failure.
>   * @param qstate: qstate with region.
> + * @param vq: validator qstate with attempt counts.
>   * @param reasonbuf: buffer to use for fail reason string print.
>   * @param reasonlen: length of reasonbuf.
>   * @return a KeyEntry. This will either contain the now trusted
> @@ -253,7 +264,7 @@ struct key_entry_key* val_verify_new_DNS
>      struct ub_packed_rrset_key* ta_ds_rrset,
>      struct ub_packed_rrset_key* ta_dnskey_rrset, int downprot,
>      char** reason, sldns_ede_code *reason_bogus, struct module_qstate*
> qstate,
> -    char* reasonbuf, size_t reasonlen);
> +    struct val_qstate* vq, char* reasonbuf, size_t reasonlen);
>
>  /**
>   * Determine if DS rrset is usable for validator or not.
> @@ -315,6 +326,16 @@ void val_fill_reply(struct reply_info* c
>         size_t cname_skip, uint8_t* name, size_t len, uint8_t* signer);
>
>  /**
> + * Remove rrsets with index .. index+count from reply, from the answer
> section.
> + * @param rep: reply to remove it from.
> + * @param index: rrset to remove, must be in the answer section.
> + * @param count: number of rrsets to remove, starting from the index.
> + *     with count=1, it removes only the index rrset.
> + */
> +void val_reply_remove_answers(struct reply_info* rep, size_t index,
> +       size_t count);
> +
> +/**
>   * Remove rrset with index from reply, from the authority section.
>   * @param rep: reply to remove it from.
>   * @param index: rrset to remove, must be in the authority section.
> @@ -441,5 +462,8 @@ int derive_cname_from_dname(struct ub_pa
>  /** Get signer name from RRSIG, sname is NULL if malformed. */
>  void rrsig_get_signer(uint8_t* data, size_t len, uint8_t** sname,
>         size_t* slen);
> +
> +/** See if the NSEC nextowner name is a subdomain of the name. */
> +int nsec_nextowner_subdomain(struct ub_packed_rrset_key* rrset, uint8_t*
> name);
>
>  #endif /* VALIDATOR_VAL_UTILS_H */
> Index: validator/validator.c
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/validator.c,v
> diff -u -p -r1.30 validator.c
> --- validator/validator.c       27 Jul 2026 14:14:39 -0000      1.30
> +++ validator/validator.c       20 Sep 2026 09:50:48 -0000
> @@ -68,6 +68,9 @@
>  #define MAX_VALIDATE_AT_ONCE 8
>  /** Max number of validation suspends allowed, error out otherwise. */
>  #define MAX_VALIDATION_SUSPENDS 16
> +/** Max answer RRsets for qtype ANY that are validated. The lists is
> + * shortened to fit this limit. */
> +#define MAX_RRSETS_ANY_VALIDATED 24
>
>  /* forward decl for cache response and normal super inform calls of a DS
> */
>  static void process_ds_response(struct module_qstate* qstate,
> @@ -347,13 +350,17 @@ static void
>  val_restart(struct val_qstate* vq)
>  {
>         struct comm_timer* temp_timer;
> -       int restart_count;
> +       int restart_count, num_validation_attempts, num_hash_attempts;
>         if(!vq) return;
>         temp_timer = vq->suspend_timer;
>         restart_count = vq->restart_count+1;
> +       num_validation_attempts = vq->num_validation_attempts;
> +       num_hash_attempts = vq->num_hash_attempts;
>         memset(vq, 0, sizeof(*vq));
>         vq->suspend_timer = temp_timer;
>         vq->restart_count = restart_count;
> +       vq->num_validation_attempts = num_validation_attempts;
> +       vq->num_hash_attempts = num_hash_attempts;
>         vq->state = VAL_INIT_STATE;
>  }
>
> @@ -452,6 +459,24 @@ already_validated(struct dns_msg* ret_ms
>         return 0;
>  }
>
> +/** If it is possible to restart the validation state */
> +static int
> +val_can_restart(struct module_qstate* qstate, struct val_qstate* vq,
> +       struct val_env* ve)
> +{
> +       /* For validation failures that are limits exceeded on the amount
> +        * of work that the DNSSEC validator is willing to do, the restart
> +        * is not allowed. A restart would increase the amount of effort
> +        * spent even further. */
> +       if(vq->restart_count < ve->max_restart &&
> +               vq->num_validation_attempts <=
> qstate->env->cfg->val_validation_attempts &&
> +               vq->num_hash_attempts <=
> qstate->env->cfg->val_hash_attempts &&
> +               !vq->num_nsec_attempts_exceeded)
> +               return 1;
> +       (void)qstate;
> +       return 0;
> +}
> +
>  /**
>   * Generate a request for DNS data.
>   *
> @@ -760,8 +785,8 @@ validate_msg_signatures(struct module_qs
>
>                 /* Verify the answer rrset */
>                 sec = val_verify_rrset_entry(env, ve, s, key_entry,
> &reason,
> -                       &reason_bogus, LDNS_SECTION_ANSWER, qstate,
> &verified,
> -                       reasonbuf, sizeof(reasonbuf));
> +                       &reason_bogus, LDNS_SECTION_ANSWER, qstate, vq,
> +                       &verified, reasonbuf, sizeof(reasonbuf));
>                 /* If the (answer) rrset failed to validate, then this
>                  * message is BAD. */
>                 if(sec != sec_status_secure) {
> @@ -805,7 +830,7 @@ validate_msg_signatures(struct module_qs
>                         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, vq,
>                         &verified, reasonbuf, sizeof(reasonbuf));
>                 /* If anything in the authority section fails to be
> secure,
>                  * we have a bad message. */
> @@ -852,7 +877,7 @@ validate_msg_signatures(struct module_qs
>                 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,
> -                               &verified, reasonbuf, sizeof(reasonbuf));
> +                               vq, &verified, reasonbuf,
> sizeof(reasonbuf));
>                 /* the additional section can fail to be secure,
>                  * it is optional, check signature in case we need
>                  * to clean the additional section later. */
> @@ -921,10 +946,10 @@ validate_suspend_setup_timer(struct modu
>                 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;
> +       /* One step of back-off after the first suspend so a single bad
> +        * message still yields, but does not grow exponentially on its
> own. */
> +       if(vq->suspend_count > 0)
> +               slack += 1;
>         if(slack != 0 && slack <= 12 /* No numeric overflow. */) {
>                 usec = usec << slack;
>         }
> @@ -1025,6 +1050,29 @@ remove_spurious_authority(struct reply_i
>  }
>
>  /**
> + * Cap the number of answer RRsets for validation of type ANY.
> + * This limits the number of RRSIG validations performed.
> + * It is allowed to return a subset of available RRsets when processing
> + * ANY query.
> + * @param chase_reply: the chased reply, shorten if if too long.
> + * @param orig_reply: original reply, remove the records here as well,
> + *     so it can be marked as DNSSEC valid.
> + * @param skip: the number of rrsets skipped in the answer section due to
> + *     CNAME chain that is followed.
> + * @param max_rrsets: the number allowed.
> + */
> +static void
> +shorten_answer_any(struct reply_info* chase_reply,
> +       struct reply_info* orig_reply, size_t skip, size_t max_rrsets)
> +{
> +       if(chase_reply->an_numrrsets > max_rrsets) {
> +               size_t to_rem = chase_reply->an_numrrsets - max_rrsets;
> +               val_reply_remove_answers(chase_reply, max_rrsets, to_rem);
> +               val_reply_remove_answers(orig_reply, skip+max_rrsets,
> to_rem);
> +       }
> +}
> +
> +/**
>   * Given a "positive" response -- a response that contains an answer to
> the
>   * question, and no CNAME chain, validate this response.
>   *
> @@ -1407,16 +1455,20 @@ validate_nameerror_response(struct modul
>   * trusted DNSKEY rrset that signs this response must already have been
>   * completed.
>   *
> + * @param env: module env.
>   * @param chase_reply: answer to validate.
>   */
>  static void
> -validate_referral_response(struct reply_info* chase_reply)
> +validate_referral_response(struct module_env* env, struct reply_info*
> chase_reply)
>  {
> -       size_t i;
> +       size_t i, count;
>         enum sec_status s;
>         /* message security equals lowest rrset security */
>         chase_reply->security = sec_status_secure;
> -       for(i=0; i<chase_reply->rrset_count; i++) {
> +       if(env->cfg->val_clean_additional)
> +               count = chase_reply->rrset_count;
> +       else    count =
> chase_reply->an_numrrsets+chase_reply->ns_numrrsets;
> +       for(i=0; i<count; i++) {
>                 s = ((struct packed_rrset_data*)chase_reply->rrsets[i]
>                         ->entry.data)->security;
>                 if(s < chase_reply->security)
> @@ -2283,7 +2335,7 @@ processValidate(struct module_qstate* qs
>                         key_entry_get_reason_bogus(vq->key_entry));
>                 errinf_ede(qstate, "while building chain of trust",
>                         key_entry_get_reason_bogus(vq->key_entry));
> -               if(vq->restart_count >= ve->max_restart)
> +               if(!val_can_restart(qstate, vq, ve))
>                         key_cache_insert(ve->kcache, vq->key_entry,
>                                 qstate->env->cfg->val_log_level >= 2);
>                 return 1;
> @@ -2306,6 +2358,9 @@ processValidate(struct module_qstate* qs
>                 &vq->qchase, vq->orig_msg->rep, vq->rrset_skip);
>         if(subtype != VAL_CLASS_REFERRAL)
>                 remove_spurious_authority(vq->chase_reply,
> vq->orig_msg->rep);
> +       if(subtype == VAL_CLASS_ANY)
> +               shorten_answer_any(vq->chase_reply, vq->orig_msg->rep,
> +                       vq->rrset_skip, MAX_RRSETS_ANY_VALIDATED);
>
>         /* check signatures in the message;
>          * answer and authority must be valid, additional is only checked.
> */
> @@ -2428,7 +2483,7 @@ processValidate(struct module_qstate* qs
>
>                 case VAL_CLASS_REFERRAL:
>                         verbose(VERB_ALGO, "Validating a referral
> response");
> -                       validate_referral_response(vq->chase_reply);
> +                       validate_referral_response(qstate->env,
> vq->chase_reply);
>                         verbose(VERB_DETAIL, "validate(referral): %s",
>                                 sec_status_to_string(
>                                 vq->chase_reply->security));
> @@ -2502,15 +2557,17 @@ processFinished(struct module_qstate* qs
>         }
>
>         if(subtype == VAL_CLASS_REFERRAL) {
> -               /* for a referral, move to next unchecked rrset and check
> it*/
> -               vq->rrset_skip = val_next_unchecked(vq->orig_msg->rep,
> -                       vq->rrset_skip);
> -               if(vq->rrset_skip < vq->orig_msg->rep->rrset_count) {
> -                       /* and restart for this rrset */
> -                       verbose(VERB_ALGO, "validator: go to next rrset");
> -                       vq->chase_reply->security = sec_status_unchecked;
> -                       vq->state = VAL_INIT_STATE;
> -                       return 1;
> +               if(qstate->env->cfg->val_clean_additional) {
> +                       /* for a referral, move to next unchecked rrset
> and check it*/
> +                       vq->rrset_skip =
> val_next_unchecked(vq->orig_msg->rep,
> +                               vq->rrset_skip);
> +                       if(vq->rrset_skip <
> vq->orig_msg->rep->rrset_count) {
> +                               /* and restart for this rrset */
> +                               verbose(VERB_ALGO, "validator: go to next
> rrset");
> +                               vq->chase_reply->security =
> sec_status_unchecked;
> +                               vq->state = VAL_INIT_STATE;
> +                               return 1;
> +                       }
>                 }
>                 /* referral chase is done */
>         }
> @@ -2555,7 +2612,7 @@ processFinished(struct module_qstate* qs
>                 struct msgreply_entry* e;
>
>                 /* see if we can try again to fetch data */
> -               if(vq->restart_count < ve->max_restart) {
> +               if(val_can_restart(qstate, vq, ve)) {
>                         verbose(VERB_ALGO, "validation failed, "
>                                 "blacklist and retry to fetch data");
>                         val_blacklist(&qstate->blacklist, qstate->region,
> @@ -2847,6 +2904,7 @@ val_operate(struct module_qstate* qstate
>   *     (this rrset is allocated in the wrong region, not the qstate).
>   * @param ta: trust anchor.
>   * @param qstate: qstate that needs key.
> + * @param vq: validator qstate.
>   * @param id: module id.
>   * @param sub_qstate: the sub query state, that is the lookup that fetched
>   *     the trust anchor data, it contains error information for the
> answer.
> @@ -2857,8 +2915,8 @@ val_operate(struct module_qstate* qstate
>   */
>  static struct key_entry_key*
>  primeResponseToKE(struct ub_packed_rrset_key* dnskey_rrset,
> -       struct trust_anchor* ta, struct module_qstate* qstate, int id,
> -       struct module_qstate* sub_qstate)
> +       struct trust_anchor* ta, struct module_qstate* qstate,
> +       struct val_qstate* vq, int id, struct module_qstate* sub_qstate)
>  {
>         struct val_env* ve = (struct val_env*)qstate->env->modinfo[id];
>         struct key_entry_key* kkey = NULL;
> @@ -2898,7 +2956,8 @@ primeResponseToKE(struct ub_packed_rrset
>         /* attempt to verify with trust anchor DS and DNSKEY */
>         kkey = val_verify_new_DNSKEYs_with_ta(qstate->region, qstate->env,
> ve,
>                 dnskey_rrset, ta->ds_rrset, ta->dnskey_rrset, downprot,
> -               &reason, &reason_bogus, qstate, reasonbuf,
> sizeof(reasonbuf));
> +               &reason, &reason_bogus, qstate, vq, reasonbuf,
> +               sizeof(reasonbuf));
>         if(!kkey) {
>                 log_err("out of memory: verifying prime TA");
>                 return NULL;
> @@ -3011,7 +3070,7 @@ ds_response_to_ke(struct module_qstate*
>                  * bogus, then we are done. */
>                 sec = val_verify_rrset_entry(qstate->env, ve, ds,
>                         vq->key_entry, &reason, &reason_bogus,
> -                       LDNS_SECTION_ANSWER, qstate, &verified, reasonbuf,
> +                       LDNS_SECTION_ANSWER, qstate, vq, &verified,
> reasonbuf,
>                         sizeof(reasonbuf));
>                 if(sec != sec_status_secure) {
>                         verbose(VERB_DETAIL, "DS rrset in DS response did "
> @@ -3062,7 +3121,7 @@ ds_response_to_ke(struct module_qstate*
>                 /* Try to prove absence of the DS with NSEC */
>                 sec = val_nsec_prove_nodata_dsreply(
>                         qstate->env, ve, qinfo, msg->rep, vq->key_entry,
> -                       &proof_ttl, &reason, &reason_bogus, qstate,
> +                       &proof_ttl, &reason, &reason_bogus, qstate, vq,
>                         reasonbuf, sizeof(reasonbuf));
>                 switch(sec) {
>                         case sec_status_secure:
> @@ -3100,7 +3159,7 @@ ds_response_to_ke(struct module_qstate*
>                 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, &vq->nsec3_cache_table,
> +                       &reason_bogus, qstate, vq, &vq->nsec3_cache_table,
>                         reasonbuf, sizeof(reasonbuf));
>                 switch(sec) {
>                         case sec_status_insecure:
> @@ -3168,7 +3227,7 @@ ds_response_to_ke(struct module_qstate*
>                 }
>                 sec = val_verify_rrset_entry(qstate->env, ve, cname,
>                         vq->key_entry, &reason, &reason_bogus,
> -                       LDNS_SECTION_ANSWER, qstate, &verified, reasonbuf,
> +                       LDNS_SECTION_ANSWER, qstate, vq, &verified,
> reasonbuf,
>                         sizeof(reasonbuf));
>                 if(sec == sec_status_secure) {
>                         /* Check for wildcard expansion */
> @@ -3289,6 +3348,7 @@ process_ds_response(struct module_qstate
>         uint8_t* olds = vq->empty_DS_name;
>         int ret;
>         *suspend = 0;
> +       vq->num_nsec_attempts = 0;
>         vq->empty_DS_name = NULL;
>         if(sub_qstate && sub_qstate->rpz_applied) {
>                 verbose(VERB_ALGO, "rpz was applied to the DS lookup, "
> @@ -3300,6 +3360,8 @@ process_ds_response(struct module_qstate
>         }
>         ret = ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske,
>                 sub_qstate);
> +       /* New NSEC attempt count for next message validation. */
> +       vq->num_nsec_attempts = 0;
>         if(ret != 0) {
>                 switch(ret) {
>                 case 1:
> @@ -3341,7 +3403,7 @@ process_ds_response(struct module_qstate
>                 vq->chain_blacklist = NULL; /* fresh blacklist for next
> part*/
>                 /* Keep the forState.state on FINDKEY. */
>         } else if(key_entry_isbad(dske)
> -               && vq->restart_count < ve->max_restart) {
> +               && val_can_restart(qstate, vq, ve)) {
>                 vq->empty_DS_name = olds;
>                 val_blacklist(&vq->chain_blacklist, qstate->region,
> origin, 1);
>                 qstate->errinf = NULL;
> @@ -3391,6 +3453,7 @@ process_dnskey_response(struct module_qs
>         char* reason = NULL;
>         sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
>
> +       vq->num_nsec_attempts = 0;
>         if(sub_qstate && sub_qstate->rpz_applied) {
>                 verbose(VERB_ALGO, "rpz was applied to the DNSKEY lookup, "
>                         "make it insecure");
> @@ -3410,7 +3473,7 @@ process_dnskey_response(struct module_qs
>                 verbose(VERB_DETAIL, "Missing DNSKEY RRset in response to "
>                         "DNSKEY query.");
>
> -               if(vq->restart_count < ve->max_restart) {
> +               if(val_can_restart(qstate, vq, ve)) {
>                         val_blacklist(&vq->chain_blacklist, qstate->region,
>                                 origin, 1);
>                         qstate->errinf = NULL;
> @@ -3447,7 +3510,9 @@ process_dnskey_response(struct module_qs
>         downprot = qstate->env->cfg->harden_algo_downgrade;
>         vq->key_entry = val_verify_new_DNSKEYs(qstate->region, qstate->env,
>                 ve, dnskey, vq->ds_rrset, downprot, &reason, &reason_bogus,
> -               qstate, reasonbuf, sizeof(reasonbuf));
> +               qstate, vq, reasonbuf, sizeof(reasonbuf));
> +       /* New NSEC attempt count for next message validation. */
> +       vq->num_nsec_attempts = 0;
>
>         if(!vq->key_entry) {
>                 log_err("out of memory in verify new DNSKEYs");
> @@ -3458,7 +3523,7 @@ process_dnskey_response(struct module_qs
>          * state. */
>         if(!key_entry_isgood(vq->key_entry)) {
>                 if(key_entry_isbad(vq->key_entry)) {
> -                       if(vq->restart_count < ve->max_restart) {
> +                       if(val_can_restart(qstate, vq, ve)) {
>                                 val_blacklist(&vq->chain_blacklist,
>                                         qstate->region, origin, 1);
>                                 qstate->errinf = NULL;
> @@ -3510,6 +3575,7 @@ process_prime_response(struct module_qst
>         struct trust_anchor* ta = anchor_find(qstate->env->anchors,
>                 vq->trust_anchor_name, vq->trust_anchor_labs,
>                 vq->trust_anchor_len, vq->qchase.qclass);
> +       vq->num_nsec_attempts = 0;
>         if(!ta) {
>                 /* trust anchor revoked, restart with less anchors */
>                 vq->state = VAL_INIT_STATE;
> @@ -3528,19 +3594,23 @@ process_prime_response(struct module_qst
>
>         if(ta->autr) {
>                 if(!autr_process_prime(qstate->env, ve, ta, dnskey_rrset,
> -                       qstate)) {
> +                       qstate, vq)) {
> +                       /* New NSEC attempt count for next message
> validation. */
> +                       vq->num_nsec_attempts = 0;
>                         /* trust anchor revoked, restart with less anchors
> */
>                         vq->state = VAL_INIT_STATE;
>                         vq->trust_anchor_name = NULL;
>                         return;
>                 }
>         }
> -       vq->key_entry = primeResponseToKE(dnskey_rrset, ta, qstate, id,
> +       vq->key_entry = primeResponseToKE(dnskey_rrset, ta, qstate, vq, id,
>                 sub_qstate);
>         lock_basic_unlock(&ta->lock);
> +       /* New NSEC attempt count for next message validation. */
> +       vq->num_nsec_attempts = 0;
>         if(vq->key_entry) {
>                 if(key_entry_isbad(vq->key_entry)
> -                       && vq->restart_count < ve->max_restart) {
> +                       && val_can_restart(qstate, vq, ve)) {
>                         val_blacklist(&vq->chain_blacklist,
> qstate->region,
>                                 origin, 1);
>                         qstate->errinf = NULL;
> Index: validator/validator.h
> ===================================================================
> RCS file: /cvs/src/usr.sbin/unbound/validator/validator.h,v
> diff -u -p -r1.11 validator.h
> --- validator/validator.h       31 Aug 2025 21:41:10 -0000      1.11
> +++ validator/validator.h       20 Sep 2026 09:50:48 -0000
> @@ -231,6 +231,19 @@ struct val_qstate {
>         struct comm_timer* suspend_timer;
>         /** Number of suspends */
>         int suspend_count;
> +
> +       /** Number of DNSKEY RRSIG validation attempts. This is the number
> of
> +        * cryptographic operations done for the mesh state. */
> +       int num_validation_attempts;
> +       /** Number of DS hash verification attempts. This is the number of
> +        * hash operations done for the mesh state.
> +        * It does not count NSEC3 hashes. */
> +       int num_hash_attempts;
> +       /** Number of NSEC validations. And NSEC3 too. This is reset per
> +        * answer. */
> +       int num_nsec_attempts;
> +       /** The nsec attempts have been exceeded. */
> +       int num_nsec_attempts_exceeded;
>  };
>
>  /**
>
>

-- 
Kindest regards,
Tom Smyth.