Download raw body.
unbound 1.26.1
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, ¶ms, 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, ¶ms,
> 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.
unbound 1.26.1