Download raw body.
unbound 1.26.1
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;
};
/**
unbound 1.26.1