From eb86d9bc50c5d3a7206522cef5ba7594a6a6d58e Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:11:14 +0200 Subject: [PATCH 01/11] Set version to 1.26.1 --- configure | 25 +++++++++++++------------ configure.ac | 5 +++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/configure b/configure index 8c0a416cd..699d67006 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for unbound 1.26.0. +# Generated by GNU Autoconf 2.71 for unbound 1.26.1. # # Report bugs to . # @@ -622,8 +622,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='unbound' PACKAGE_TARNAME='unbound' -PACKAGE_VERSION='1.26.0' -PACKAGE_STRING='unbound 1.26.0' +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='' @@ -1513,7 +1513,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.26.0 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]... @@ -1579,7 +1579,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of unbound 1.26.0:";; + short | recursive ) echo "Configuration of unbound 1.26.1:";; esac cat <<\_ACEOF @@ -1832,7 +1832,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -unbound configure 1.26.0 +unbound configure 1.26.1 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2489,7 +2489,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.26.0, 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 @@ -3253,11 +3253,11 @@ UNBOUND_VERSION_MAJOR=1 UNBOUND_VERSION_MINOR=26 -UNBOUND_VERSION_MICRO=0 +UNBOUND_VERSION_MICRO=1 LIBUNBOUND_CURRENT=9 -LIBUNBOUND_REVISION=39 +LIBUNBOUND_REVISION=40 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -3364,6 +3364,7 @@ LIBUNBOUND_AGE=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 @@ -25711,7 +25712,7 @@ printf "%s\n" "#define MAXSYSLOGMSGLEN 10240" >>confdefs.h -version=1.26.0 +version=1.26.1 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for build time" >&5 printf %s "checking for build time... " >&6; } @@ -26241,7 +26242,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # 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.26.0, 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 @@ -26309,7 +26310,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -unbound config.status 1.26.0 +unbound config.status 1.26.1 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index 254a6c630..afedb889e 100644 --- a/configure.ac +++ b/configure.ac @@ -12,14 +12,14 @@ sinclude(dnscrypt/dnscrypt.m4) # must be numbers. ac_defun because of later processing m4_define([VERSION_MAJOR],[1]) m4_define([VERSION_MINOR],[26]) -m4_define([VERSION_MICRO],[0]) +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=39 +LIBUNBOUND_REVISION=40 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -126,6 +126,7 @@ LIBUNBOUND_AGE=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 From e619ead2dbcde8fc47f03b2ac68987b3377da4c7 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:16:29 +0200 Subject: [PATCH 02/11] - Fix CVE-2026-80225, Possible degradation of service from continuous queries on the same TCP/DoT connection. Thanks to Qifan Zhang from Palo Alto Networks for the report. --- services/listen_dnsport.c | 12 ++++++- services/listen_dnsport.h | 8 ++++- testcode/testbound.c | 10 ++++++ util/fptr_wlist.c | 2 ++ util/netevent.c | 66 ++++++++++++++++++++++++++++++++++++++- util/netevent.h | 9 ++++++ 6 files changed, 104 insertions(+), 3 deletions(-) diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index 8ef084e5e..4d1c17373 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -2133,7 +2133,7 @@ void listen_start_accept(struct listen_dnsport* listen) } 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* spoolbuf) 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* req) { 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 */ @@ -2189,6 +2196,9 @@ void tcp_req_info_clear(struct tcp_req_info* req) 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 diff --git a/services/listen_dnsport.h b/services/listen_dnsport.h index ae0463468..4aeb6aa70 100644 --- a/services/listen_dnsport.h +++ b/services/listen_dnsport.h @@ -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. diff --git a/testcode/testbound.c b/testcode/testbound.c index 063037df4..3a3bcaef6 100644 --- a/testcode/testbound.c +++ b/testcode/testbound.c @@ -786,3 +786,13 @@ size_t doq_table_quic_size_get(struct doq_table* ATTR_UNUSED(table)) return 0; } #endif + +void tcp_read_again_cb(void* ATTR_UNUSED(arg)) +{ + /* nothing */ +} + +void tcp_more_read_again_cb(void* ATTR_UNUSED(arg)) +{ + /* nothing */ +} diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c index 0ff6840ad..1be742bd5 100644 --- a/util/fptr_wlist.c +++ b/util/fptr_wlist.c @@ -141,6 +141,8 @@ fptr_whitelist_comm_timer(void (*fptr)(void*)) #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; diff --git a/util/netevent.c b/util/netevent.c index 432fead1b..6b1957dc3 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -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.*/ @@ -4621,6 +4625,10 @@ comm_point_tcp_handle_write(int fd, struct comm_point* c) 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; @@ -4637,6 +4645,16 @@ tcp_req_info_read_again(int fd, struct comm_point* 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; } @@ -4650,6 +4668,7 @@ tcp_more_read_again(int fd, struct comm_point* c) /* 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)) { @@ -4662,6 +4681,30 @@ tcp_more_read_again(int fd, struct comm_point* c) } 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; + } } } @@ -4689,6 +4732,23 @@ tcp_more_write_again(int fd, struct comm_point* c) } } +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) { @@ -6131,7 +6191,7 @@ comm_point_create_tcp_handler(struct comm_base *base, 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); @@ -6693,6 +6753,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) { @@ -6732,6 +6795,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 diff --git a/util/netevent.h b/util/netevent.h index 7d64fac5b..7f53464b1 100644 --- a/util/netevent.h +++ b/util/netevent.h @@ -384,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. @@ -1133,6 +1136,12 @@ void doq_send_pkt(struct comm_point* c, struct doq_pkt_addr* paddr, /** 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. * handle libevent callback for timer comm. From 8c2e0fd6cc5b6eaa618ac72227808824b4edee7c Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:18:49 +0200 Subject: [PATCH 03/11] - Fix CVE-2026-81642, Heap buffer overflow and possible Remote Code Execution when digesting DNSKEY. Thanks to Yuqi Qiu and Xiang Li from Nankai University, AOSP Lab for the report. --- validator/val_sigcrypt.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 4139cc1fe..3cb0b6b45 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -305,6 +305,8 @@ ds_create_dnskey_digest(struct module_env* env, * 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)); From eba3d35ad4ecd07aaf114a7434bcc8a097e938a8 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:24:29 +0200 Subject: [PATCH 04/11] - Fix CVE-2026-82720, Use-after-free in DoH stream cleanup code path. Thanks to Yuqi Qiu and Xiang Li from Nankai University, AOSP Lab, for the report. --- services/mesh.c | 143 ++++++++++++++++++++++++------------------------ util/netevent.c | 21 +++++++ 2 files changed, 92 insertions(+), 72 deletions(-) diff --git a/services/mesh.c b/services/mesh.c index add773b88..6c52759ae 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -1097,6 +1097,18 @@ mesh_state_make_unique(struct mesh_state* mstate) 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) { @@ -1112,15 +1124,30 @@ mesh_state_cleanup(struct mesh_state* mstate) } /* drop unsent replies */ if(!mstate->replies_sent) { - struct mesh_reply* rep = mstate->reply_list; + struct mesh_reply* rep; struct mesh_cb* cb; - /* 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--; @@ -1484,12 +1511,6 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, 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) @@ -1546,9 +1567,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, 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; @@ -1570,9 +1589,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, } 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; @@ -1608,9 +1625,7 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, 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); @@ -1762,6 +1777,7 @@ void mesh_query_done(struct mesh_state* mstate) 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) { @@ -1786,7 +1802,18 @@ void mesh_query_done(struct mesh_state* mstate) && (!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 @@ -1801,22 +1828,18 @@ void mesh_query_done(struct mesh_state* mstate) ((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++; @@ -1841,22 +1864,17 @@ void mesh_query_done(struct mesh_state* mstate) /* 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 { @@ -1897,18 +1915,6 @@ void mesh_query_done(struct mesh_state* mstate) } } - /* 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) { @@ -2498,7 +2504,6 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, } } - static int apply_respip_action(struct module_qstate* qstate, const struct query_info* qinfo, struct respip_client_info* cinfo, @@ -2631,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); @@ -2639,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++; @@ -2712,18 +2723,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 */ diff --git a/util/netevent.c b/util/netevent.c index 6b1957dc3..47e7c4b78 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -3230,6 +3230,26 @@ static int http2_submit_settings(struct http2_session* h2_session) } #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, @@ -6936,6 +6956,7 @@ comm_point_drop_reply(struct comm_reply* repinfo) 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; From 7914901915dd46817e7ae9877776d6fb6fc2060e Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:25:05 +0200 Subject: [PATCH 05/11] - Fix CVE-2026-78227, Use-after-free in DoQ stream output buffer on reset re-transmission. Thanks to Yuqi Qiu and Xiang Li from Nankai University, AOSP Lab for the report. --- services/listen_dnsport.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index 4d1c17373..f545a2aaf 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -4514,7 +4514,7 @@ doq_stream_reset_cb(ngtcp2_conn* ATTR_UNUSED(conn), int64_t stream_id, "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; } From 565651cd02d1f7c70cbbd1bbc40d9f130ee38651 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:26:00 +0200 Subject: [PATCH 06/11] - Fix CVE-2026-77955, Possible ZONEMD verification bypass window. Thanks to Yuqi Qiu and Xiang Li from Nankai University, AOSP Lab, for the report. In addition, thanks to Qifan Zhang from Palo Alto Networks for also reporting this issue. --- services/authzone.c | 235 +++++++++++++++++------- services/authzone.h | 4 + testdata/auth_zonemd_xfr_chain_fail.rpl | 32 +--- 3 files changed, 177 insertions(+), 94 deletions(-) diff --git a/services/authzone.c b/services/authzone.c index 72b37fef9..b3fb09e64 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -392,6 +392,20 @@ auth_data_del(rbnode_type* n, void* ATTR_UNUSED(arg)) 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) @@ -413,6 +427,7 @@ auth_zone_delete(struct auth_zone* z, struct auth_zones* az) } if(z->rpz) rpz_delete(z->rpz); + auth_chunk_list_delete(z->perform_write_chunk_list); free(z->name); free(z->zonefile); free(z); @@ -2369,14 +2384,7 @@ 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; @@ -3593,7 +3601,13 @@ int auth_zones_lookup(struct auth_zones* az, struct query_info* qinfo, *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; @@ -3695,7 +3709,10 @@ int auth_zones_downstream_answer(struct auth_zones* az, struct module_env* env, 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; @@ -5299,7 +5316,7 @@ apply_http(struct auth_xfer* xfr, struct auth_zone* z, /** 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; @@ -5308,7 +5325,7 @@ auth_zone_write_chunks(struct auth_xfer* xfr, const char* fname) 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); @@ -5319,14 +5336,91 @@ auth_zone_write_chunks(struct auth_xfer* xfr, const char* fname) 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; + + if(z->zonefile == NULL || z->zonefile[0] == 0) { + /* no write needed, no zonefile set */ + auth_chunk_list_delete(chunk_list); + return; + } + zfilename = z->zonefile; + if(cfg->chrootdir && cfg->chrootdir[0] && strncmp(zfilename, + cfg->chrootdir, strlen(cfg->chrootdir)) == 0) + zfilename += strlen(cfg->chrootdir); + if(verbosity >= VERB_ALGO) { + char nm[LDNS_MAX_DOMAINLEN]; + dname_str(z->name, nm); + verbose(VERB_ALGO, "write zonefile %s for %s", zfilename, nm); + } + + /* write to tempfile first */ + if((size_t)strlen(zfilename) + 16 > sizeof(tmpfile)) { + verbose(VERB_ALGO, "tmpfilename too long, cannot update " + " zonefile %s", zfilename); + auth_chunk_list_delete(chunk_list); + return; + } + snprintf(tmpfile, sizeof(tmpfile), "%s.tmp%u", zfilename, + (unsigned)getpid()); + if(chunk_list) { + /* use the stored chunk list to write them */ + if(!auth_zone_write_chunks(chunk_list, tmpfile)) { + unlink(tmpfile); + auth_chunk_list_delete(chunk_list); + return; + } + auth_chunk_list_delete(chunk_list); + } else if(!auth_zone_write_file(z, tmpfile)) { + unlink(tmpfile); + return; + } +#ifdef UB_ON_WINDOWS + (void)unlink(zfilename); /* windows does not replace file with rename() */ +#endif + if(rename(tmpfile, zfilename) < 0) { + log_err("could not rename(%s, %s): %s", tmpfile, zfilename, + strerror(errno)); + unlink(tmpfile); + 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 @@ -5344,52 +5438,7 @@ xfr_write_after_update(struct auth_xfer* xfr, struct module_env* env) 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 */ - return; - } - zfilename = z->zonefile; - if(cfg->chrootdir && cfg->chrootdir[0] && strncmp(zfilename, - cfg->chrootdir, strlen(cfg->chrootdir)) == 0) - zfilename += strlen(cfg->chrootdir); - if(verbosity >= VERB_ALGO) { - char nm[LDNS_MAX_DOMAINLEN]; - dname_str(z->name, nm); - verbose(VERB_ALGO, "write zonefile %s for %s", zfilename, nm); - } - - /* write to tempfile first */ - if((size_t)strlen(zfilename) + 16 > sizeof(tmpfile)) { - verbose(VERB_ALGO, "tmpfilename too long, cannot update " - " zonefile %s", zfilename); - lock_rw_unlock(&z->lock); - return; - } - snprintf(tmpfile, sizeof(tmpfile), "%s.tmp%u", zfilename, - (unsigned)getpid()); - if(xfr->task_transfer->master->http) { - /* use the stored chunk list to write them */ - if(!auth_zone_write_chunks(xfr, tmpfile)) { - unlink(tmpfile); - lock_rw_unlock(&z->lock); - return; - } - } else if(!auth_zone_write_file(z, tmpfile)) { - unlink(tmpfile); - lock_rw_unlock(&z->lock); - return; - } -#ifdef UB_ON_WINDOWS - (void)unlink(zfilename); /* windows does not replace file with rename() */ -#endif - if(rename(tmpfile, zfilename) < 0) { - log_err("could not rename(%s, %s): %s", tmpfile, zfilename, - strerror(errno)); - unlink(tmpfile); - lock_rw_unlock(&z->lock); - return; - } + zone_write_after_update(z, env, chunk_list); lock_rw_unlock(&z->lock); } @@ -5422,6 +5471,8 @@ xfr_process_chunk_list(struct auth_xfer* xfr, struct module_env* env, 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); @@ -5505,6 +5556,25 @@ xfr_process_chunk_list(struct auth_xfer* xfr, struct module_env* env, 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); @@ -5515,7 +5585,9 @@ xfr_process_chunk_list(struct auth_xfer* xfr, struct module_env* env, (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; } @@ -8601,9 +8673,13 @@ void auth_zonemd_dnskey_lookup_callback(void* arg, int rcode, sldns_buffer* buf, 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; @@ -8726,7 +8802,37 @@ void auth_zonemd_dnskey_lookup_callback(void* arg, int rcode, sldns_buffer* buf, 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 */ @@ -8794,6 +8900,9 @@ zonemd_lookup_dnskey(struct auth_zone* z, struct module_env* env) &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; diff --git a/services/authzone.h b/services/authzone.h index 97ab01fcb..24c8c80b5 100644 --- a/services/authzone.h +++ b/services/authzone.h @@ -146,6 +146,10 @@ struct auth_zone { 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 */ diff --git a/testdata/auth_zonemd_xfr_chain_fail.rpl b/testdata/auth_zonemd_xfr_chain_fail.rpl index 3e09c9e8e..76a989f75 100644 --- a/testdata/auth_zonemd_xfr_chain_fail.rpl +++ b/testdata/auth_zonemd_xfr_chain_fail.rpl @@ -283,39 +283,9 @@ www.example.com. IN A SECTION ANSWER ENTRY_END -; the zonefile was updated with new contents +; the zonefile was not updated with new contents, due to zonemd failure STEP 70 CHECK_TEMPFILE example.com FILE_BEGIN -example.com. 3600 IN SOA ns.example.com. hostmaster.example.com. 200154054 28800 7200 604800 3600 -example.com. 3600 IN RRSIG SOA 8 2 3600 20201116135527 20201019135527 55566 example.com. gcFHT/Q4iDZ78CK6fyY2HZr8sRtgH2Rna9fEs06RW0gqMnfDntweoIaBamOZ7NlAP84aY2bZeanmEccmkHexByUpodCoKQ4NzVXctLr0TO4PVoFyfUfj62fjhM56SF8ioDxsoDQcPtYXcjNQjwfntWofMqHCMxrb9LzbgePzhOM= -example.com. 3600 IN NS ns.example.com. -example.com. 3600 IN RRSIG NS 8 2 3600 20201116135527 20201019135527 55566 example.com. X+V3XsbJbBi9OsHpjMkGCox8RLY/uXp/XX/O/flTrIre9fMDWm9ZGnewtuQFpLgGc6hUTi0eLsuRWRA5fZXEKUBhmoR2Ph01KgE1gvlL7v6zPWQwXVcBRUr3mOSbYdNNkHkXEjiDBGEhNkfqR216zNgw563eEGXOkLUFNIx5Zpg= -example.com. 3600 IN NSEC bar.example.com. NS SOA RRSIG NSEC DNSKEY ZONEMD -example.com. 3600 IN RRSIG NSEC 8 2 3600 20201116135527 20201019135527 55566 example.com. ufLrlOQprAqjnH85Rt3T0Mxd3ZB0mBeeNIr84eFJ8Rk6WiWEPm0Y1R7GRufNI24Mj7iqLcL4nJM6KK6B7dJqjqu73jw1acuYNnbsoV2BNDRXRFP2FNWTpctVdi+955f3FzgsmEJXfGiSUG0YXAEcZmdCPCn5ii2jk8mk7r6KKYo= -example.com. 3600 IN DNSKEY 256 3 8 AwEAAdug/L739i0mgN2nuK/bhxu3wFn5Ud9nK2+XUmZQlPUEZUC5YZvm1rfMmEWTGBn87fFxEu/kjFZHJ55JLzqsbbpVHLbmKCTT2gYR2FV2WDKROGKuYbVkJIXdKAjJ0ONuK507NinYvlWXIoxHn22KAWOd9wKgSTNHBlmGkX+ts3hh ;{id = 55566} -example.com. 3600 IN RRSIG DNSKEY 8 2 3600 20201116135527 20201019135527 55566 example.com. fsdnVg38PKQTH2mDOwkXL6Jre7JP7Gf8WI3CvIbmeYQUJtAlpcSbZkS3wInm3kKMxOuT55BWzndQzpfmpo91OqJjG27W0k9301NMLUwFprA6b9HK+iPAT0JpYPDPzcm1bQdarLzLS+eD/GPwmyVSX7Gze+08VfE8m8sOW2r7UjA= -example.com. 3600 IN ZONEMD 200154054 1 2 58F7620F93204BBB31B44F795B3409CC4ABD9EF5601DECC15675BD7751213152984EDDCE0626E6062E744B03B3E47711202FBB79E4A2EB8BC5CF46741B5CAE6F -example.com. 3600 IN RRSIG ZONEMD 8 2 3600 20201116135527 20201019135527 55566 example.com. orn8ZF/yqj9u4WrhiO6gtEcTaVsnZSWWZLfXhcIOiWSB8kKCxtZl5cG17dD3Du1NllUwMRqkp0KleLhIoUS9xeQ/0x05u+CYLrfQ62oAiD7q54ZQzpXJIH52aQzKV70ZnO03CZowhQBnetmIoKX6xLogKo8pt+BdQbo3oVHxV8Y= -bar.example.com. 3600 IN A 1.2.3.55 -bar.example.com. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55566 example.com. NYhmRicF4C9+YxpWeQrepy4ALM1CM0USoDuGi3W5Xtp4/+YpCJfSIdR9vlJaJ2WayYuZrz9Ai2ci7oWwE1Fn3oywGwCKvGo9m0c3mC2eEtphE19wrop6pWu6um4RiFhmzYS1voraA3PAdYzze9U4NHzlk0+sb5vNZW9dSZS30Ds= -bar.example.com. 3600 IN NSEC ding.example.com. A RRSIG NSEC -bar.example.com. 3600 IN RRSIG NSEC 8 3 3600 20201116135527 20201019135527 55566 example.com. VhsGuBx20DXQZNU8ITAMnasn6NVyEjN9xtB8msH5xJn80UCuaqvFBURzcPWN3aHnykEvGfdPF/9P3WvlON0cMikWkqSLy6Q9bpvgAq13HWYh+ZcDoqLtICaB7RkBQc+6aHAqZFyQbD8/m8Kxt5eVJtV6rEuf+yPX0+3aXHhsRg0= -ding.example.com. 3600 IN A 1.2.3.4 -ding.example.com. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55566 example.com. OERsruISkpd1s68ute8Xm8YXisBCTkkiDMt34K+0dVqvySOJq63d3qN18BeUxZxLyHDB1eR3nZZKqEdkTqrv2r98skhWhjnOECpFbu5gKjtN/KPexbbJ+rxC0QqciuWOC7M6YE0cvI17/RB9KhVRy5rqY2X4Gt2wk2CNeD1dAko= -ding.example.com. 3600 IN NSEC foo.example.com. A RRSIG NSEC -ding.example.com. 3600 IN RRSIG NSEC 8 3 3600 20201116135527 20201019135527 55566 example.com. nb1W2aaKrU5iAQiY8gMsoMOejID19JMTEwY2rRoe+KsvzMs0rE0ifEkqit4blXaU0tfy0foJ70uqdJFqBoGz1NcSwZ6GNk/iNfGvG3XpxZ/zqEe7kkIucqqei794G7z9psqV94yZ3WaT+IswPpWrSaWv1w41RtcWufPhe4fOAmU= -foo.example.com. 3600 IN A 1.2.3.4 -foo.example.com. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55566 example.com. ZcUngb2pUejwnsshbJN/Dfr+Bzu8fcZXyqLArQ+10Bw1IPHyfx7yyUJ43V5tTYVHPSEsJzTnaWj+olVrNhVZxq5e0pgzSYPfGln2FEItEvMIOn33j8yKTpPW2MLyuFF5ZkXhosG20EUwRMvMmRHRz9mIZfwWoMbSGPukmLh8zMA= -foo.example.com. 3600 IN NSEC ns.example.com. A RRSIG NSEC -foo.example.com. 3600 IN RRSIG NSEC 8 3 3600 20201116135527 20201019135527 55566 example.com. fUZEpkEULRWDntN5Z7Kr8M83Hjhf08ECMKRpo6IBoBc3ayenj+YMgWAvFXC825wjENPYYWNGag0d32U83zCZxqgv+8uXZd3B7QDpTbL41aWZdc++s5YWTkYjyOWwJ1XHOv4nL3qEnJBXVzo/E1gbSKhTFuG97i+7J1MFd9MsC5s= -ns.example.com. 3600 IN A 127.0.0.1 -ns.example.com. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55566 example.com. SiuxuPtN/ITd+Z20j8UNUHJWbLHirE8zQOWMv5fAZ1rPKpAidrZgUL8J417GdrTwkueU2ywAJ7EzFJSwNTa7o/wUnq7svmOR6Ze6UQsKuZFZGEfqPNDRp4YuF86LU5jChuo+f/IRpydHrxVwGxDPCR9KarDM+ewfW+yI5bZeZcg= -ns.example.com. 3600 IN NSEC www.example.com. A RRSIG NSEC -ns.example.com. 3600 IN RRSIG NSEC 8 3 3600 20201116135527 20201019135527 55566 example.com. 0upKNYjiow4NDJm3I1RbUddE9GGuFYEVKswww5BAc/6WHuukupncL30lskvcSKGpByDssP2Hi2CufyEtYeGWh6q1TxtOFRqFBX1p6Q5b3tBlCtvv4h31dQR9uqLvq+GkGS5MR+0LO5kWagIpZmnI8YY5plVdXEtNbp2Ar8zvz/A= -www.example.com. 3600 IN A 127.0.0.1 -www.example.com. 3600 IN RRSIG A 8 3 3600 20201116135527 20201019135527 55566 example.com. AaIeICaPjV50TDrpbyOn94+hs8EYIMTmN4pYqj7e8GIGimqQIk5jgpwSx6SOoOF+uOqkf9GKHkQTn5YVGaeXwEQleg7mPTmMYKAOk06Y7MFUO1Vwt1Vt7Wo+Cpa3x2a1CmEkfFOi4WqP43VJnUtjjKmXoKRz3VUmqByyJYUAGbQ= -www.example.com. 3600 IN NSEC example.com. A RRSIG NSEC -www.example.com. 3600 IN RRSIG NSEC 8 3 3600 20201116135527 20201019135527 55566 example.com. meg/t6nIBqQZ0d5/dT7uu/3CuP4vE+HxqFQaj2fjUNceA/6C7QIQnqQ5Kyblg+XijDkQX0yvyFNHYdgF16UDgFT7tlNUCHk1SpF5BWzV4c4tBEhxASTz7UQo111O3Tyd6CldPzO/Se15Ud0/ZYltHEqWTfY5nJoXC/OJD9V2QOI= FILE_END SCENARIO_END From 3d65973d38cd7e4dcdc9d5764ef1ea40fd849d10 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:27:17 +0200 Subject: [PATCH 07/11] - Fix CVE-2026-81634, Possible heap buffer overflow during DNSSEC canonicalization. Thanks to Vlatko Kosturjak with Marlink Cyber, for the report. --- testcode/unitverify.c | 55 ++++++++++++++++++++++++++++++++++++++++ validator/val_sigcrypt.c | 41 ++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/testcode/unitverify.c b/testcode/unitverify.c index fcf2e2ffe..eb1f43c3c 100644 --- a/testcode/unitverify.c +++ b/testcode/unitverify.c @@ -510,6 +510,60 @@ 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); +} + #define xstr(s) str(s) #define str(s) #s @@ -724,4 +778,5 @@ verify_test(void) #endif nsectest(); nsec3_hash_test(SRCDIRSTR "/testdata/test_nsec3_hash.1"); + canon_owner_buf_test(); } diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 3cb0b6b45..8c0568c9f 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -1310,15 +1310,32 @@ rrset_canonical(struct regional* region, sldns_buffer* buf, } 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 */ 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; } @@ -1327,6 +1344,13 @@ rrset_canonical(struct regional* region, sldns_buffer* buf, 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); @@ -1378,11 +1402,17 @@ rrset_canonicalize_to_buffer(struct regional* region, sldns_buffer* buf, 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; } @@ -1395,6 +1425,13 @@ rrset_canonicalize_to_buffer(struct regional* region, sldns_buffer* buf, 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]); From 3a6ba0da81536020dc2244f6da6d6a9d60d4e063 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:28:11 +0200 Subject: [PATCH 08/11] - Unit test for CVE-2026-81642 fix. --- testcode/unitverify.c | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/testcode/unitverify.c b/testcode/unitverify.c index eb1f43c3c..db0a1a383 100644 --- a/testcode/unitverify.c +++ b/testcode/unitverify.c @@ -564,6 +564,92 @@ canon_owner_buf_test(void) 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 @@ -778,5 +864,6 @@ verify_test(void) #endif nsectest(); nsec3_hash_test(SRCDIRSTR "/testdata/test_nsec3_hash.1"); + dnskey_ds_digest_test(); canon_owner_buf_test(); } From bd71e3b8a6dd87682bf454dc3687fc9e70b1bb78 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:29:04 +0200 Subject: [PATCH 09/11] - Fix CVE-2026-77860, 'serve-expired' can bypass Unbound 'wait-limit'. Thanks to Xuanchao Xie, Lutong Chen, and Kaiping Xue from the University of Science and Technology of China (USTC) for the report. --- services/mesh.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/services/mesh.c b/services/mesh.c index 6c52759ae..0360a0a86 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -2703,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; } From 0d4a6a63dd71050d216a467b488e28176f2599df Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:29:32 +0200 Subject: [PATCH 10/11] - Fix CVE-2026-82717, CNAME synthesis could lead to heap corruption. Thanks to Ben Morris from Anthropic for the report. --- iterator/iter_scrub.c | 9 ++++++++- util/data/dname.c | 21 ++++++++++++++------- util/data/msgreply.c | 34 +++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index f2f20a5c1..1e3e01330 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -294,7 +294,14 @@ synth_cname_rrset(uint8_t** sname, size_t* snamelen, uint8_t* alias, 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); diff --git a/util/data/dname.c b/util/data/dname.c index 5370aa6f9..9fbbe1092 100644 --- a/util/data/dname.c +++ b/util/data/dname.c @@ -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 diff --git a/util/data/msgreply.c b/util/data/msgreply.c index 0beb893c3..71cab7d74 100644 --- a/util/data/msgreply.c +++ b/util/data/msgreply.c @@ -248,6 +248,7 @@ rdata_copy(sldns_buffer* pkt, struct packed_rrset_data* data, uint8_t* to, 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 packed_rrset_data* data, uint8_t* to, (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 packed_rrset_data* data, uint8_t* to, 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 packed_rrset_data* data, uint8_t* to, 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 packed_rrset_data* data, uint8_t* to, } } /* 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; } From 13b6717f1716810523ffb60f971a6c9c5e8ce127 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 16 Sep 2026 09:30:25 +0200 Subject: [PATCH 11/11] - Fix CVE-2026-85501, Retrap: Novel Vulnerabilities to launch Algorithmic Complexity Attacks on DNSSEC. Thanks to Zuyao Xu and Xiang Li from Nankai University, AOSP Lab for the report. In addition, thanks to Qifan Zhang from Palo Alto Networks for a complimentary report. --- daemon/remote.c | 2 + daemon/worker.c | 2 +- doc/example.conf.in | 8 +- doc/unbound.conf.rst | 22 +++- iterator/iter_scrub.c | 11 ++ services/authzone.c | 9 +- services/cache/dns.c | 2 +- services/mesh.c | 2 +- testcode/unitverify.c | 2 +- testdata/03-testbound.tdir/03-testbound.test | 9 ++ testdata/val_refer_unsignadd.rpl | 1 + testdata/val_referglue.rpl | 1 + util/config_file.c | 8 +- util/config_file.h | 4 + util/configlexer.lex | 2 + util/configparser.y | 22 +++- util/data/msgreply.c | 11 ++ util/data/msgreply.h | 3 + validator/autotrust.c | 22 ++-- validator/autotrust.h | 4 +- validator/val_nsec.c | 11 +- validator/val_nsec.h | 5 +- validator/val_nsec3.c | 10 +- validator/val_nsec3.h | 5 +- validator/val_sigcrypt.c | 73 ++++++++++++- validator/val_sigcrypt.h | 12 +- validator/val_utils.c | 105 ++++++++++++++---- validator/val_utils.h | 21 +++- validator/validator.c | 109 +++++++++++++------ validator/validator.h | 13 +++ 30 files changed, 411 insertions(+), 100 deletions(-) diff --git a/daemon/remote.c b/daemon/remote.c index 8e6ba19b1..23e62d2ec 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -6590,6 +6590,8 @@ fr_atomic_copy_cfg(struct config_file* oldcfg, struct config_file* cfg, 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); diff --git a/daemon/worker.c b/daemon/worker.c index 5cc586442..8cfc776c6 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -863,7 +863,7 @@ answer_from_cache(struct worker* worker, struct query_info* qinfo, "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) { diff --git a/doc/example.conf.in b/doc/example.conf.in index c5e4e242e..4687dc64f 100644 --- a/doc/example.conf.in +++ b/doc/example.conf.in @@ -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, diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index e341c516f..838840d13 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -2320,7 +2320,7 @@ These options are part of the ``server:`` section. 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@@: ** @@ -3358,6 +3358,26 @@ These options are part of the ``server:`` section. Default: yes +@@UAHL@unbound.conf@val-validation-attempts@@: ** + 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@@: ** + 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@@: ** Specify how many times out of 1000 to pick from the set of fastest servers. 0 turns the feature off. diff --git a/iterator/iter_scrub.c b/iterator/iter_scrub.c index 1e3e01330..d1f3896bf 100644 --- a/iterator/iter_scrub.c +++ b/iterator/iter_scrub.c @@ -655,6 +655,9 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, 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; @@ -674,6 +677,9 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, 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. */ @@ -798,6 +804,11 @@ scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, 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 */ diff --git a/services/authzone.c b/services/authzone.c index b3fb09e64..3fe2cec53 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -8234,7 +8234,8 @@ static int zonemd_dnssec_verify_rrset(struct auth_zone* z, "zonemd: verify %s RRset with DNSKEY", typestr); } sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg, why_bogus, NULL, - LDNS_SECTION_ANSWER, NULL, &verified, reasonbuf, reasonlen); + LDNS_SECTION_ANSWER, NULL, NULL, &verified, reasonbuf, + reasonlen); if(sec == sec_status_secure) { return 1; } @@ -8583,8 +8584,8 @@ zonemd_get_dnskey_from_anchor(struct auth_zone* z, struct module_env* env, 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 */ @@ -8644,7 +8645,7 @@ auth_zone_verify_zonemd_key_with_ds(struct auth_zone* z, 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 */ diff --git a/services/cache/dns.c b/services/cache/dns.c index 04d5ae2d6..e83b57c11 100644 --- a/services/cache/dns.c +++ b/services/cache/dns.c @@ -677,7 +677,7 @@ tomsg(struct module_env* env, struct query_info* q, struct reply_info* r, 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; diff --git a/services/mesh.c b/services/mesh.c index 0360a0a86..04dedd329 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -373,7 +373,7 @@ mesh_serve_expired_lookup(struct module_qstate* qstate, "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 */ diff --git a/testcode/unitverify.c b/testcode/unitverify.c index db0a1a383..ea991faac 100644 --- a/testcode/unitverify.c +++ b/testcode/unitverify.c @@ -196,7 +196,7 @@ verifytest_rrset(struct module_env* env, struct val_env* ve, setup_sigalg(dnskey, sigalg); /* check all algorithms in the dnskey */ /* ok to give null as qstate here, won't be used for answer section. */ sec = dnskeyset_verify_rrset(env, ve, rrset, dnskey, sigalg, &reason, - NULL, LDNS_SECTION_ANSWER, NULL, &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), diff --git a/testdata/03-testbound.tdir/03-testbound.test b/testdata/03-testbound.tdir/03-testbound.test index b9fdf214d..dd75b061d 100644 --- a/testdata/03-testbound.tdir/03-testbound.test +++ b/testdata/03-testbound.tdir/03-testbound.test @@ -85,6 +85,15 @@ for input in $PRE/testdata/*.rpl $PRE/testdata/*.crpl; do fi fi + # detect if ECDSA is needed + if echo $cleaninput | grep ecdsa >/dev/null 2>&1; then + if $PRE/testbound -e >/dev/null 2>&1; then + : # ECDSA is supported + else + continue + fi + fi + # detect if CLIENT_SUBNET is needed if echo $cleaninput | grep subnet >/dev/null 2>&1; then if $PRE/testbound -c >/dev/null 2>&1; then diff --git a/testdata/val_refer_unsignadd.rpl b/testdata/val_refer_unsignadd.rpl index 22f15d21a..6e3418ceb 100644 --- a/testdata/val_refer_unsignadd.rpl +++ b/testdata/val_refer_unsignadd.rpl @@ -11,6 +11,7 @@ server: trust-anchor-signaling: no iter-scrub-promiscuous: no rrset-roundrobin: no + val-clean-additional: yes stub-zone: name: "." diff --git a/testdata/val_referglue.rpl b/testdata/val_referglue.rpl index 3ca0c0e80..c14bad17f 100644 --- a/testdata/val_referglue.rpl +++ b/testdata/val_referglue.rpl @@ -12,6 +12,7 @@ server: minimal-responses: no iter-scrub-promiscuous: no rrset-roundrobin: no + val-clean-additional: yes stub-zone: name: "." diff --git a/util/config_file.c b/util/config_file.c index ab209a818..623f02ad3 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -278,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; @@ -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); @@ -785,6 +787,8 @@ int config_set_option(struct config_file* cfg, const char* opt, 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) @@ -1265,6 +1269,8 @@ config_get_option(struct config_file* cfg, const char* opt, 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) diff --git a/util/config_file.h b/util/config_file.h index c2b45759f..ae7eb6436 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -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; diff --git a/util/configlexer.lex b/util/configlexer.lex index 196575bdb..84e353d5e 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -608,6 +608,8 @@ iter-scrub-ns{COLON} { YDVAR(1, VAR_ITER_SCRUB_NS) } 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) } diff --git a/util/configparser.y b/util/configparser.y index 490bba0ff..1f877df0b 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -218,6 +218,7 @@ extern struct config_parser_state* cfg_parser; %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 %% @@ -360,7 +361,8 @@ content_server: server_num_threads | server_verbosity | server_port | 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 @@ -4317,6 +4319,24 @@ server_iter_scrub_promiscuous: VAR_ITER_SCRUB_PROMISCUOUS STRING_ARG 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); + } + ; ipsetstart: VAR_IPSET { OUTYY(("\nP(ipset:)\n")); diff --git a/util/data/msgreply.c b/util/data/msgreply.c index 71cab7d74..86193a671 100644 --- a/util/data/msgreply.c +++ b/util/data/msgreply.c @@ -1139,6 +1139,17 @@ reply_all_rrsets_secure(struct reply_info* rep) return 1; } +int reply_an_ns_rrsets_secure(struct reply_info* rep) +{ + size_t i; + for(i=0; ian_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) diff --git a/util/data/msgreply.h b/util/data/msgreply.h index e7c688e97..e4476660d 100644 --- a/util/data/msgreply.h +++ b/util/data/msgreply.h @@ -494,6 +494,9 @@ int reply_check_cname_chain(struct query_info* qinfo, struct reply_info* rep); */ 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. diff --git a/validator/autotrust.c b/validator/autotrust.c index fc7897b71..bfe0319af 100644 --- a/validator/autotrust.c +++ b/validator/autotrust.c @@ -1297,12 +1297,13 @@ void autr_write_file(struct module_env* env, struct trust_anchor* tp) * @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; @@ -1310,7 +1311,7 @@ verify_dnskey(struct module_env* env, struct val_env* ve, 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 @@ -1350,16 +1351,18 @@ min_expiry(struct module_env* env, struct packed_rrset_data* dd) 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); } @@ -1575,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; @@ -1595,7 +1598,8 @@ check_contains_revoked(struct module_env* env, struct val_env* ve, } 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) */ @@ -2209,7 +2213,7 @@ autr_tp_remove(struct module_env* env, struct trust_anchor* tp, 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); @@ -2250,7 +2254,7 @@ int autr_process_prime(struct module_env* env, struct val_env* ve, 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)) { @@ -2266,7 +2270,7 @@ int autr_process_prime(struct module_env* env, struct val_env* ve, } } /* 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 */ diff --git a/validator/autotrust.h b/validator/autotrust.h index 057f2b68a..2e2f92668 100644 --- a/validator/autotrust.h +++ b/validator/autotrust.h @@ -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_anchor* tp); * @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 diff --git a/validator/val_nsec.c b/validator/val_nsec.c index becd42b4d..a7f49d6e1 100644 --- a/validator/val_nsec.c +++ b/validator/val_nsec.c @@ -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, struct val_env* ve, 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 module_env* env, 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 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,7 +222,7 @@ val_nsec_prove_nodata_dsreply(struct module_env* env, struct val_env* ve, * 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; @@ -258,7 +259,7 @@ val_nsec_prove_nodata_dsreply(struct module_env* env, struct val_env* ve, 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 " diff --git a/validator/val_nsec.h b/validator/val_nsec.h index c1d45314a..a9002b78a 100644 --- a/validator/val_nsec.h +++ b/validator/val_nsec.h @@ -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_dsreply(struct module_env* env, 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. diff --git a/validator/val_nsec3.c b/validator/val_nsec3.c index d0385be68..8314f2db9 100644 --- a/validator/val_nsec3.c +++ b/validator/val_nsec3.c @@ -1521,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; @@ -1537,7 +1538,7 @@ list_is_secure(struct module_env* env, struct val_env* ve, 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; @@ -1552,7 +1553,8 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve, struct ub_packed_rrset_key** list, size_t num, struct query_info* qinfo, struct key_entry_key* kkey, char** reason, sldns_ede_code* reason_bogus, struct module_qstate* qstate, - 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; @@ -1568,7 +1570,7 @@ nsec3_prove_nods(struct module_env* env, struct val_env* ve, 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 */ } diff --git a/validator/val_nsec3.h b/validator/val_nsec3.h index a13e92991..b6f56741f 100644 --- a/validator/val_nsec3.h +++ b/validator/val_nsec3.h @@ -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* env, struct val_env* ve, * @param reason: string for bogus result. * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure. * @param qstate: qstate with region. + * @param 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 val_env* ve, struct ub_packed_rrset_key** list, size_t num, struct query_info* qinfo, struct key_entry_key* kkey, char** reason, sldns_ede_code* reason_bogus, struct module_qstate* qstate, - 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. diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 8c0568c9f..612754cf3 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -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 @@ -548,8 +550,10 @@ int algo_needs_missing(struct algo_needs* n) * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure. * @param section: section of packet where this rrset comes from. * @param qstate: qstate with region. + * @param 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). @@ -561,7 +565,7 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve, 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; @@ -580,6 +584,14 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve, } for(i=0; i 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)) @@ -587,6 +599,26 @@ dnskeyset_verify_rrset_sig(struct module_env* env, struct val_env* ve, 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, @@ -626,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; @@ -658,9 +691,19 @@ dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve, } } for(i=0; i 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) @@ -709,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; @@ -730,9 +774,26 @@ dnskey_verify_rrset(struct module_env* env, struct val_env* ve, } for(i=0; i 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, diff --git a/validator/val_sigcrypt.h b/validator/val_sigcrypt.h index c1e2658e4..b2cfae96d 100644 --- a/validator/val_sigcrypt.h +++ b/validator/val_sigcrypt.h @@ -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_packed_rrset_key* k, size_t idx); * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure. * @param section: section of packet where this rrset comes from. * @param qstate: qstate with region. + * @param 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(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); /** * verify rrset against one specific dnskey (from rrset) @@ -287,13 +290,16 @@ enum sec_status dnskeyset_verify_rrset(struct module_env* env, * @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 diff --git a/validator/val_utils.c b/validator/val_utils.c index e77f93f5a..c5fb7f325 100644 --- a/validator/val_utils.c +++ b/validator/val_utils.c @@ -406,7 +406,8 @@ val_verify_rrset(struct module_env* env, struct val_env* ve, struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* keys, uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus, sldns_pkt_section section, struct module_qstate* qstate, - 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, struct val_env* ve, 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* env, struct val_env* ve, struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus, sldns_pkt_section section, struct module_qstate* qstate, - 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* env, struct val_env* ve, dnskey.entry.key = &dnskey; dnskey.entry.data = kd->rrset_data; sec = val_verify_rrset(env, ve, rrset, &dnskey, kd->algo, reason, - reason_bogus, section, qstate, verified, reasonbuf, reasonlen); + reason_bogus, section, qstate, vq, verified, reasonbuf, + reasonlen); return sec; } @@ -499,13 +503,20 @@ verify_dnskeys_with_ds_rr(struct module_env* env, struct val_env* ve, 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 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_env* env, struct val_env* ve, 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_env* env, struct val_env* ve, /* 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_env* env, 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) { /* 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_env* env, struct val_env* ve, } num = rrset_get_count(ds_rrset); for(i=0; i 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_env* env, struct val_env* ve, 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* region, struct module_env* env, 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_env* env, struct val_env* ve, 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_env* env, struct val_env* ve, if(ta_ds) { num = rrset_get_count(ta_ds); for(i=0; i 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_env* env, struct val_env* ve, 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_env* env, struct val_env* ve, /* 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 regional* region, struct module_env* env, 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) { diff --git a/validator/val_utils.h b/validator/val_utils.h index 43386edbf..e5e3194a9 100644 --- a/validator/val_utils.h +++ b/validator/val_utils.h @@ -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_classification subtype, * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure. * @param section: section of packet where this rrset comes from. * @param qstate: qstate with region. + * @param 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(struct module_env* env, struct val_env* ve, struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus, sldns_pkt_section section, struct module_qstate* qstate, - 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(struct module_env* env, * @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_DS(struct module_env* env, 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 @@ -174,6 +182,7 @@ enum sec_status val_verify_DNSKEY_with_DS(struct module_env* env, * @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. @@ -185,7 +194,7 @@ enum sec_status val_verify_DNSKEY_with_TA(struct module_env* env, 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_TA(struct module_env* env, * @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_DNSKEYs(struct regional* region, 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_DNSKEYs(struct regional* region, * @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_DNSKEYs_with_ta(struct regional* region, 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. diff --git a/validator/validator.c b/validator/validator.c index d3ed8be3e..4958a2242 100644 --- a/validator/validator.c +++ b/validator/validator.c @@ -350,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; } @@ -455,6 +459,24 @@ already_validated(struct dns_msg* ret_msg) 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. * @@ -763,8 +785,8 @@ validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq, /* 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) { @@ -808,7 +830,7 @@ validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq, 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. */ @@ -855,7 +877,7 @@ validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq, 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. */ @@ -1433,16 +1455,20 @@ validate_nameerror_response(struct module_env* env, struct val_env* ve, * 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; irrset_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; irrsets[i] ->entry.data)->security; if(s < chase_reply->security) @@ -2309,7 +2335,7 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq, 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; @@ -2457,7 +2483,7 @@ processValidate(struct module_qstate* qstate, struct val_qstate* vq, 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)); @@ -2531,15 +2557,17 @@ processFinished(struct module_qstate* qstate, struct val_qstate* vq, } 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 */ } @@ -2584,7 +2612,7 @@ processFinished(struct module_qstate* qstate, struct val_qstate* vq, 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, @@ -2876,6 +2904,7 @@ val_operate(struct module_qstate* qstate, enum module_ev event, int id, * (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. @@ -2886,8 +2915,8 @@ val_operate(struct module_qstate* qstate, enum module_ev event, int id, */ 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; @@ -2927,7 +2956,8 @@ primeResponseToKE(struct ub_packed_rrset_key* dnskey_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; @@ -3040,7 +3070,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq, * 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 " @@ -3091,7 +3121,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq, /* 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: @@ -3129,7 +3159,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq, 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: @@ -3197,7 +3227,7 @@ ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq, } 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 */ @@ -3318,6 +3348,7 @@ process_ds_response(struct module_qstate* qstate, struct val_qstate* vq, 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, " @@ -3329,6 +3360,8 @@ process_ds_response(struct module_qstate* qstate, struct val_qstate* vq, } 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: @@ -3370,7 +3403,7 @@ process_ds_response(struct module_qstate* qstate, struct val_qstate* vq, 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; @@ -3420,6 +3453,7 @@ process_dnskey_response(struct module_qstate* qstate, struct val_qstate* vq, 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"); @@ -3439,7 +3473,7 @@ process_dnskey_response(struct module_qstate* qstate, struct val_qstate* vq, 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; @@ -3476,7 +3510,9 @@ process_dnskey_response(struct module_qstate* qstate, struct val_qstate* vq, 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"); @@ -3487,7 +3523,7 @@ process_dnskey_response(struct module_qstate* qstate, struct val_qstate* vq, * 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; @@ -3539,6 +3575,7 @@ process_prime_response(struct module_qstate* qstate, struct val_qstate* vq, 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; @@ -3557,19 +3594,23 @@ process_prime_response(struct module_qstate* qstate, struct val_qstate* vq, 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; diff --git a/validator/validator.h b/validator/validator.h index e04fad572..f7bc3550a 100644 --- a/validator/validator.h +++ b/validator/validator.h @@ -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; }; /**