From 87d59bfcedef7fe9d3253594e836ed50c6d00de1 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:06:30 +0200 Subject: [PATCH 01/26] Set version to 1.25.2 --- configure | 25 +++++++++++++------------ configure.ac | 5 +++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/configure b/configure index f9d46ea15..fd1e143e5 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.25.1. +# Generated by GNU Autoconf 2.71 for unbound 1.25.2. # # Report bugs to . # @@ -622,8 +622,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='unbound' PACKAGE_TARNAME='unbound' -PACKAGE_VERSION='1.25.1' -PACKAGE_STRING='unbound 1.25.1' +PACKAGE_VERSION='1.25.2' +PACKAGE_STRING='unbound 1.25.2' 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.25.1 to adapt to many kinds of systems. +\`configure' configures unbound 1.25.2 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.25.1:";; + short | recursive ) echo "Configuration of unbound 1.25.2:";; 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.25.1 +unbound configure 1.25.2 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.25.1, which was +It was created by unbound $as_me 1.25.2, 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=25 -UNBOUND_VERSION_MICRO=1 +UNBOUND_VERSION_MICRO=2 LIBUNBOUND_CURRENT=9 -LIBUNBOUND_REVISION=37 +LIBUNBOUND_REVISION=38 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -3362,6 +3362,7 @@ LIBUNBOUND_AGE=1 # 1.24.2 had 9:35:1 # 1.25.0 had 9:36:1 # 1.25.1 had 9:37:1 +# 1.25.2 had 9:38:1 # Current -- the number of the binary API that we're implementing # Revision -- which iteration of the implementation of the binary @@ -25552,7 +25553,7 @@ printf "%s\n" "#define MAXSYSLOGMSGLEN 10240" >>confdefs.h -version=1.25.1 +version=1.25.2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for build time" >&5 printf %s "checking for build time... " >&6; } @@ -26082,7 +26083,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.25.1, which was +This file was extended by unbound $as_me 1.25.2, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -26150,7 +26151,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.25.1 +unbound config.status 1.25.2 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index f1c5b0419..27e16f646 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],[25]) -m4_define([VERSION_MICRO],[1]) +m4_define([VERSION_MICRO],[2]) 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=37 +LIBUNBOUND_REVISION=38 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -124,6 +124,7 @@ LIBUNBOUND_AGE=1 # 1.24.2 had 9:35:1 # 1.25.0 had 9:36:1 # 1.25.1 had 9:37:1 +# 1.25.2 had 9:38:1 # Current -- the number of the binary API that we're implementing # Revision -- which iteration of the implementation of the binary From fea0ff550bb6193417c9b17ffff409eb6736f90d Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:07:52 +0200 Subject: [PATCH 02/26] - Fix CVE-2026-46582, A wildcard replay, as another piece of data, triggers poisoning in the serve expired reply path. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- validator/val_utils.c | 15 ++++++++++++--- validator/validator.c | 31 ++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/validator/val_utils.c b/validator/val_utils.c index 8e4c91900..405cf897f 100644 --- a/validator/val_utils.c +++ b/validator/val_utils.c @@ -439,10 +439,15 @@ val_verify_rrset(struct module_env* env, struct val_env* ve, * only improves security status * and bogus is set only once, even if we rechecked the status */ if(sec > d->security) { + int wc_expanded = 0; d->security = sec; - if(sec == sec_status_secure) + if(sec == sec_status_secure) { + uint8_t* wc = NULL; + size_t wclen = 0; d->trust = rrset_trust_validated; - else if(sec == sec_status_bogus) { + if(val_rrset_wildcard(rrset, &wc, &wclen) && wc) + wc_expanded = 1; + } else if(sec == sec_status_bogus) { size_t i; /* update ttl for rrset to fixed value. */ d->ttl = ve->bogus_ttl; @@ -455,7 +460,11 @@ val_verify_rrset(struct module_env* env, struct val_env* ve, lock_basic_unlock(&ve->bogus_lock); } /* if status updated - store in cache for reuse */ - rrset_update_sec_status(env->rrset_cache, rrset, *env->now); + /* For a wildcard rrset, that is secure, do not store this + * into the cache, because it changes proofs around the + * item. */ + if(!wc_expanded) + rrset_update_sec_status(env->rrset_cache, rrset, *env->now); } return sec; diff --git a/validator/validator.c b/validator/validator.c index e7992b6e3..8fc9ffc94 100644 --- a/validator/validator.c +++ b/validator/validator.c @@ -1044,6 +1044,9 @@ validate_positive_response(struct module_env* env, struct val_env* ve, size_t wl; int wc_cached = 0; int wc_NSEC_ok = 0; + /* This is used to update the RRset cache, with the combination + * of the dname expansion and this wildcard, for security status. */ + struct ub_packed_rrset_key* wc_rrset = NULL; int nsec3s_seen = 0; size_t i; struct ub_packed_rrset_key* s; @@ -1062,6 +1065,9 @@ validate_positive_response(struct module_env* env, struct val_env* ve, ntohs(s->rk.type), ntohs(s->rk.rrset_class)); chase_reply->security = sec_status_bogus; update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); + if(wc_rrset) + ((struct packed_rrset_data*)wc_rrset-> + entry.data)->security = sec_status_bogus; return; } if(wc && !wc_cached && env->cfg->aggressive_nsec) { @@ -1069,7 +1075,7 @@ validate_positive_response(struct module_env* env, struct val_env* ve, env->alloc, *env->now); wc_cached = 1; } - + if(wc) wc_rrset = s; } /* validate the AUTHORITY section as well - this will generally be @@ -1126,6 +1132,9 @@ validate_positive_response(struct module_env* env, struct val_env* ve, "did not exist"); chase_reply->security = sec_status_bogus; update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); + if(wc_rrset) + ((struct packed_rrset_data*)wc_rrset-> + entry.data)->security = sec_status_bogus; return; } @@ -1527,6 +1536,16 @@ validate_any_response(struct module_env* env, struct val_env* ve, "did not exist"); chase_reply->security = sec_status_bogus; update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); + /* Make the expanded name and wildcard RRSIG rrsets bogus */ + for(i=0; ian_numrrsets; i++) { + uint8_t* cwc = NULL; + size_t cwl = 0; + s = chase_reply->rrsets[i]; + if(val_rrset_wildcard(s, &cwc, &cwl) && cwc) { + ((struct packed_rrset_data*)s-> + entry.data)->security = sec_status_bogus; + } + } return; } @@ -1564,6 +1583,9 @@ validate_cname_response(struct module_env* env, struct val_env* ve, uint8_t* wc = NULL; size_t wl; int wc_NSEC_ok = 0; + /* This is used to update the RRset cache, with the combination + * of the dname expansion and this wildcard, for security status. */ + struct ub_packed_rrset_key* wc_rrset = NULL; int nsec3s_seen = 0; size_t i; struct ub_packed_rrset_key* s; @@ -1584,6 +1606,7 @@ validate_cname_response(struct module_env* env, struct val_env* ve, update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); return; } + if(wc) wc_rrset = s; /* Refuse wildcarded DNAMEs rfc 4597. * Do not follow a wildcarded DNAME because @@ -1595,6 +1618,9 @@ validate_cname_response(struct module_env* env, struct val_env* ve, ntohs(s->rk.type), ntohs(s->rk.rrset_class)); chase_reply->security = sec_status_bogus; update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); + if(wc_rrset) + ((struct packed_rrset_data*)wc_rrset-> + entry.data)->security = sec_status_bogus; return; } @@ -1659,6 +1685,9 @@ validate_cname_response(struct module_env* env, struct val_env* ve, "did not exist"); chase_reply->security = sec_status_bogus; update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS); + if(wc_rrset) + ((struct packed_rrset_data*)wc_rrset-> + entry.data)->security = sec_status_bogus; return; } From f157c691bbd7ac5a2b29d1bf30283cb043052e9a Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:08:48 +0200 Subject: [PATCH 03/26] - Fix CVE-2026-14586, Assertion in libngtcp2 when under pressure in high concurrency DNS-over-QUIC environments. Thanks to Kunta Chu, Kaihua Wang, and Jianjun Chen from Tsinghua University, for the report. --- configure.ac | 16 +++++++ services/listen_dnsport.c | 99 ++++++++++++++++++++------------------- services/listen_dnsport.h | 21 +++++---- util/netevent.c | 38 ++++++--------- util/netevent.h | 4 +- 5 files changed, 98 insertions(+), 80 deletions(-) diff --git a/configure.ac b/configure.ac index 27e16f646..a4b6b17bd 100644 --- a/configure.ac +++ b/configure.ac @@ -1736,6 +1736,22 @@ if test x_$withval = x_yes -o x_$withval != x_no; then AC_MSG_RESULT(no) ]) + AC_CHECK_DECL([CLOCK_MONOTONIC] + , [] + , [AC_MSG_ERROR([ngtcp2 for QUIC needs at least CLOCK_MONOTONIC on the system])] + , [AC_INCLUDES_DEFAULT +#ifdef TIME_WITH_SYS_TIME +# include +# include +#else +# ifdef HAVE_SYS_TIME_H +# include +# else +# include +# endif +#endif + ]) + fi # set static linking for uninstalled libraries if requested diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index 5db2b940b..a08a5352e 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -42,7 +42,6 @@ #ifdef HAVE_SYS_TYPES_H # include #endif -#include #include #ifdef USE_TCP_FASTOPEN #include @@ -3399,14 +3398,13 @@ doq_table_delete(struct doq_table* table) } struct doq_timer* -doq_timer_find_time(struct doq_table* table, struct timeval* tv) +doq_timer_find_time(struct doq_table* table, ngtcp2_tstamp ts) { struct doq_timer key; struct rbnode_type* node; log_assert(table != NULL); memset(&key, 0, sizeof(key)); - key.time.tv_sec = tv->tv_sec; - key.time.tv_usec = tv->tv_usec; + key.time_mono = ts; node = rbtree_search(table->timer_tree, &key); if(node) return (struct doq_timer*)node->key; @@ -3454,7 +3452,7 @@ doq_timer_list_remove(struct doq_table* table, struct doq_timer* timer) if(!timer->timer_in_list) return; /* The item in the rbtree has the list start and end. */ - rb_timer = doq_timer_find_time(table, &timer->time); + rb_timer = doq_timer_find_time(table, timer->time_mono); if(rb_timer) { if(timer->setlist_prev) timer->setlist_prev->setlist_next = timer->setlist_next; @@ -3500,7 +3498,8 @@ doq_timer_unset(struct doq_table* table, struct doq_timer* timer) } void doq_timer_set(struct doq_table* table, struct doq_timer* timer, - struct doq_server_socket* worker_doq_socket, struct timeval* tv) + struct doq_server_socket* worker_doq_socket, struct timeval* tv, + ngtcp2_tstamp ts) { struct doq_timer* rb_timer; if(verbosity >= VERB_ALGO && timer->conn) { @@ -3514,14 +3513,14 @@ void doq_timer_set(struct doq_table* table, struct doq_timer* timer, (int)rel.tv_sec, (int)rel.tv_usec); } if(timer->timer_in_tree || timer->timer_in_list) { - if(timer->time.tv_sec == tv->tv_sec && - timer->time.tv_usec == tv->tv_usec) + if(timer->time_mono == ts) return; /* already set on that time */ doq_timer_unset(table, timer); } - timer->time.tv_sec = tv->tv_sec; - timer->time.tv_usec = tv->tv_usec; - rb_timer = doq_timer_find_time(table, tv); + timer->time_real.tv_sec = tv->tv_sec; + timer->time_real.tv_usec = tv->tv_usec; + timer->time_mono = ts; + rb_timer = doq_timer_find_time(table, ts); if(rb_timer) { /* There is a timeout already with this value. Timer is * added to the setlist. */ @@ -3700,13 +3699,9 @@ int doq_timer_cmp(const void* key1, const void* key2) { struct doq_timer* e = (struct doq_timer*)key1; struct doq_timer* f = (struct doq_timer*)key2; - if(e->time.tv_sec < f->time.tv_sec) + if(e->time_mono < f->time_mono) return -1; - if(e->time.tv_sec > f->time.tv_sec) - return 1; - if(e->time.tv_usec < f->time.tv_usec) - return -1; - if(e->time.tv_usec > f->time.tv_usec) + if(e->time_mono > f->time_mono) return 1; return 0; } @@ -4254,12 +4249,11 @@ doq_submit_new_token(struct doq_conn* conn) ngtcp2_ssize tokenlen; int ret; const ngtcp2_path* path = ngtcp2_conn_get_path(conn->conn); - ngtcp2_tstamp ts = doq_get_timestamp_nanosec(); tokenlen = ngtcp2_crypto_generate_regular_token(token, conn->doq_socket->static_secret, conn->doq_socket->static_secret_len, path->remote.addr, - path->remote.addrlen, ts); + path->remote.addrlen, doq_get_timestamp_nanosec()); if(tokenlen < 0) { log_err("doq ngtcp2_crypto_generate_regular_token failed"); return 1; @@ -5101,23 +5095,30 @@ doq_conn_clear_conids(struct doq_conn* conn) ngtcp2_tstamp doq_get_timestamp_nanosec(void) { -#ifdef CLOCK_REALTIME struct timespec tp; memset(&tp, 0, sizeof(tp)); - /* Get a nanosecond time, that can be compared with the event base. */ - if(clock_gettime(CLOCK_REALTIME, &tp) == -1) { - log_err("clock_gettime failed: %s", strerror(errno)); +#ifdef CLOCK_BOOTTIME + if(clock_gettime(CLOCK_BOOTTIME, &tp) == -1) { +#endif + if(clock_gettime(CLOCK_MONOTONIC, &tp) == -1) { + log_err("clock_gettime failed: %s", strerror(errno)); + } +#ifdef CLOCK_BOOTTIME } +#endif return ((uint64_t)tp.tv_sec)*((uint64_t)1000000000) + ((uint64_t)tp.tv_nsec); -#else +} + +static struct timeval doq_get_timevalue(void) +{ struct timeval tv; + memset(&tv, 0, sizeof(tv)); if(gettimeofday(&tv, NULL) < 0) { log_err("gettimeofday failed: %s", strerror(errno)); + memset(&tv, 0, sizeof(tv)); } - return ((uint64_t)tv.tv_sec)*((uint64_t)1000000000) + - ((uint64_t)tv.tv_usec)*((uint64_t)1000); -#endif /* CLOCK_REALTIME */ + return tv; } /** doq start the closing period for the connection. */ @@ -5240,18 +5241,17 @@ doq_conn_recv(struct comm_point* c, struct doq_pkt_addr* paddr, int* err_drop) { int ret; - ngtcp2_tstamp ts; struct ngtcp2_path path; memset(&path, 0, sizeof(path)); path.remote.addr = (struct sockaddr*)&paddr->addr; path.remote.addrlen = paddr->addrlen; path.local.addr = (struct sockaddr*)&paddr->localaddr; path.local.addrlen = paddr->localaddrlen; - ts = doq_get_timestamp_nanosec(); ret = ngtcp2_conn_read_pkt(conn->conn, &path, pi, sldns_buffer_begin(c->doq_socket->pkt_buf), - sldns_buffer_limit(c->doq_socket->pkt_buf), ts); + sldns_buffer_limit(c->doq_socket->pkt_buf), + doq_get_timestamp_nanosec()); if(ret != 0) { if(err_retry) *err_retry = 0; @@ -5339,7 +5339,6 @@ doq_conn_write_streams(struct comm_point* c, struct doq_conn* conn, { struct doq_stream* stream = conn->stream_write_first; ngtcp2_path_storage ps; - ngtcp2_tstamp ts = doq_get_timestamp_nanosec(); size_t num_packets = 0, max_packets = 65535; ngtcp2_path_storage_zero(&ps); @@ -5392,7 +5391,8 @@ doq_conn_write_streams(struct comm_point* c, struct doq_conn* conn, ret = ngtcp2_conn_writev_stream(conn->conn, &ps.path, &pi, sldns_buffer_begin(c->doq_socket->pkt_buf), sldns_buffer_remaining(c->doq_socket->pkt_buf), - &ndatalen, flags, stream_id, datav, datav_count, ts); + &ndatalen, flags, stream_id, datav, datav_count, + doq_get_timestamp_nanosec()); if(ret < 0) { if(ret == NGTCP2_ERR_WRITE_MORE) { verbose(VERB_ALGO, "doq: write more, ndatalen %d", (int)ndatalen); @@ -5464,7 +5464,8 @@ doq_conn_write_streams(struct comm_point* c, struct doq_conn* conn, if(ret == 0) { /* congestion limited */ doq_conn_write_disable(conn); - ngtcp2_conn_update_pkt_tx_time(conn->conn, ts); + ngtcp2_conn_update_pkt_tx_time(conn->conn, + doq_get_timestamp_nanosec()); return 1; } sldns_buffer_set_position(c->doq_socket->pkt_buf, ret); @@ -5478,7 +5479,7 @@ doq_conn_write_streams(struct comm_point* c, struct doq_conn* conn, if(stream) stream = stream->write_next; } - ngtcp2_conn_update_pkt_tx_time(conn->conn, ts); + ngtcp2_conn_update_pkt_tx_time(conn->conn, doq_get_timestamp_nanosec()); return 1; } @@ -5555,32 +5556,35 @@ doq_table_pop_first(struct doq_table* table) } int -doq_conn_check_timer(struct doq_conn* conn, struct timeval* tv) +doq_conn_check_timer(struct doq_conn* conn, struct timeval* tv, ngtcp2_tstamp* ts) { - ngtcp2_tstamp expiry = ngtcp2_conn_get_expiry(conn->conn); - ngtcp2_tstamp now = doq_get_timestamp_nanosec(); + ngtcp2_tstamp doq_expiry = ngtcp2_conn_get_expiry(conn->conn); + ngtcp2_tstamp doq_now = doq_get_timestamp_nanosec(); ngtcp2_tstamp t; + struct timeval now = doq_get_timevalue(); - if(expiry <= now) { + if(doq_expiry <= doq_now || doq_expiry == UINT64_MAX) { + /* UINT64_MAX means there is no next expiry. */ /* The timer has already expired, add with zero timeout. * This should call the callback straight away. Calling it * from the event callbacks is cleaner than calling it here, * because then it is always called with the same locks and * so on. This routine only has the conn.lock. */ - t = now; + t = doq_now; + memcpy(tv, &now, sizeof(*tv)); } else { - t = expiry; + t = doq_expiry; + memset(tv, 0, sizeof(*tv)); + tv->tv_sec = (doq_expiry - doq_now) / NGTCP2_SECONDS; + tv->tv_usec = ((doq_expiry - doq_now) / NGTCP2_MICROSECONDS)%1000000; + timeval_add(tv, &now); } - /* convert to timeval */ - memset(tv, 0, sizeof(*tv)); - tv->tv_sec = t / NGTCP2_SECONDS; - tv->tv_usec = (t / NGTCP2_MICROSECONDS)%1000000; + *ts = t; /* If we already have a timer, is it the right value? */ if(conn->timer.timer_in_tree || conn->timer.timer_in_list) { - if(conn->timer.time.tv_sec == tv->tv_sec && - conn->timer.time.tv_usec == tv->tv_usec) + if(conn->timer.time_mono == *ts) return 0; } return 1; @@ -5601,13 +5605,12 @@ doq_conn_log_line(struct doq_conn* conn, char* s) int doq_conn_handle_timeout(struct doq_conn* conn) { - ngtcp2_tstamp now = doq_get_timestamp_nanosec(); int rv; if(verbosity >= VERB_ALGO) doq_conn_log_line(conn, "timeout"); - rv = ngtcp2_conn_handle_expiry(conn->conn, now); + rv = ngtcp2_conn_handle_expiry(conn->conn, doq_get_timestamp_nanosec()); if(rv != 0) { verbose(VERB_ALGO, "ngtcp2_conn_handle_expiry failed: %s", ngtcp2_strerror(rv)); diff --git a/services/listen_dnsport.h b/services/listen_dnsport.h index 963595a1c..95aa3e11e 100644 --- a/services/listen_dnsport.h +++ b/services/listen_dnsport.h @@ -538,8 +538,11 @@ void doq_table_delete(struct doq_table* table); struct doq_timer { /** The rbnode in the tree sorted by timeout value. Key this struct. */ struct rbnode_type node; + /** The timeout value. Monotonic value used with ngtcp2. + * This time value is used for the tree operations. */ + ngtcp2_tstamp time_mono; /** The timeout value. Absolute time value. */ - struct timeval time; + struct timeval time_real; /** If the timer is in the time tree, with the node. */ int timer_in_tree; /** If there are more timers with the exact same timeout value, @@ -813,10 +816,12 @@ struct doq_conn* doq_table_pop_first(struct doq_table* table); * doq check if the timer for the conn needs to be changed. * @param conn: connection, caller must hold lock on it. * @param tv: time value, absolute time, returned. + * @param ts: time stamp, absolute time, returned. * @return true if timer needs to be set to tv, false if no change is needed * to the timer. The timer is already set to the right time in that case. */ -int doq_conn_check_timer(struct doq_conn* conn, struct timeval* tv); +int doq_conn_check_timer(struct doq_conn* conn, struct timeval* tv, + ngtcp2_tstamp* ts); /** doq remove timer from tree */ void doq_timer_tree_remove(struct doq_table* table, struct doq_timer* timer); @@ -829,11 +834,12 @@ void doq_timer_unset(struct doq_table* table, struct doq_timer* timer); /** doq set the timer and add it. */ void doq_timer_set(struct doq_table* table, struct doq_timer* timer, - struct doq_server_socket* worker_doq_socket, struct timeval* tv); + struct doq_server_socket* worker_doq_socket, struct timeval* tv, + ngtcp2_tstamp ts); /** doq find a timeout in the timer tree */ struct doq_timer* doq_timer_find_time(struct doq_table* table, - struct timeval* tv); + ngtcp2_tstamp ts); /** doq handle timeout for a connection. Pass conn locked. Returns false for * deletion. */ @@ -851,6 +857,9 @@ int doq_table_quic_size_available(struct doq_table* table, /** doq get the quic size value */ size_t doq_table_quic_size_get(struct doq_table* table); + +/** get a timestamp in nanoseconds */ +ngtcp2_tstamp doq_get_timestamp_nanosec(void); #endif /* HAVE_NGTCP2 */ char* set_ip_dscp(int socket, int addrfamily, int ds); @@ -866,8 +875,4 @@ void doq_client_event_cb(int fd, short event, void* arg); /** timer event callback for testcode/doqclient */ void doq_client_timer_cb(int fd, short event, void* arg); -#ifdef HAVE_NGTCP2 -/** get a timestamp in nanoseconds */ -ngtcp2_tstamp doq_get_timestamp_nanosec(void); -#endif #endif /* LISTEN_DNSPORT_H */ diff --git a/util/netevent.c b/util/netevent.c index a86e22518..e1480dc22 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -1827,7 +1827,6 @@ doq_send_retry(struct comm_point* c, struct doq_pkt_addr* paddr, char host[256], port[32]; struct ngtcp2_cid scid; uint8_t token[NGTCP2_CRYPTO_MAX_RETRY_TOKENLEN]; - ngtcp2_tstamp ts; ngtcp2_ssize tokenlen, ret; if(!doq_print_addr_port(&paddr->addr, paddr->addrlen, host, @@ -1841,12 +1840,10 @@ doq_send_retry(struct comm_point* c, struct doq_pkt_addr* paddr, scid.datalen = c->doq_socket->sv_scidlen; doq_cid_randfill(&scid, scid.datalen, c->doq_socket->rnd); - ts = doq_get_timestamp_nanosec(); - tokenlen = ngtcp2_crypto_generate_retry_token(token, c->doq_socket->static_secret, c->doq_socket->static_secret_len, hd->version, (void*)&paddr->addr, paddr->addrlen, &scid, - &hd->dcid, ts); + &hd->dcid, doq_get_timestamp_nanosec()); if(tokenlen < 0) { log_err("ngtcp2_crypto_generate_retry_token failed: %s", ngtcp2_strerror(tokenlen)); @@ -1895,13 +1892,11 @@ doq_verify_retry_token(struct comm_point* c, struct doq_pkt_addr* paddr, struct ngtcp2_cid* ocid, struct ngtcp2_pkt_hd* hd) { char host[256], port[32]; - ngtcp2_tstamp ts; if(!doq_print_addr_port(&paddr->addr, paddr->addrlen, host, sizeof(host), port, sizeof(port))) { log_err("doq_verify_retry_token failed"); return 0; } - ts = doq_get_timestamp_nanosec(); verbose(VERB_ALGO, "doq: verifying retry token from %s %s", host, port); if(ngtcp2_crypto_verify_retry_token(ocid, @@ -1913,7 +1908,7 @@ doq_verify_retry_token(struct comm_point* c, struct doq_pkt_addr* paddr, c->doq_socket->static_secret, c->doq_socket->static_secret_len, hd->version, (void*)&paddr->addr, paddr->addrlen, &hd->dcid, - 10*NGTCP2_SECONDS, ts) != 0) { + 10*NGTCP2_SECONDS, doq_get_timestamp_nanosec()) != 0) { verbose(VERB_ALGO, "doq: could not verify retry token " "from %s %s", host, port); return 0; @@ -1928,13 +1923,11 @@ doq_verify_token(struct comm_point* c, struct doq_pkt_addr* paddr, struct ngtcp2_pkt_hd* hd) { char host[256], port[32]; - ngtcp2_tstamp ts; if(!doq_print_addr_port(&paddr->addr, paddr->addrlen, host, sizeof(host), port, sizeof(port))) { log_err("doq_verify_token failed"); return 0; } - ts = doq_get_timestamp_nanosec(); verbose(VERB_ALGO, "doq: verifying token from %s %s", host, port); if(ngtcp2_crypto_verify_regular_token( #ifdef HAVE_STRUCT_NGTCP2_PKT_HD_TOKENLEN @@ -1944,7 +1937,7 @@ doq_verify_token(struct comm_point* c, struct doq_pkt_addr* paddr, #endif c->doq_socket->static_secret, c->doq_socket->static_secret_len, (void*)&paddr->addr, paddr->addrlen, 3600*NGTCP2_SECONDS, - ts) != 0) { + doq_get_timestamp_nanosec()) != 0) { verbose(VERB_ALGO, "doq: could not verify token from %s %s", host, port); return 0; @@ -2171,6 +2164,7 @@ doq_pickup_timer(struct comm_point* c) { struct doq_timer* t; struct timeval tv; + ngtcp2_tstamp ts = 0; int have_time = 0; memset(&tv, 0, sizeof(tv)); @@ -2180,27 +2174,24 @@ doq_pickup_timer(struct comm_point* c) t->worker_doq_socket == c->doq_socket) { /* pick up this element */ t->worker_doq_socket = c->doq_socket; + memcpy(&tv, &t->time_real, sizeof(tv)); + ts = t->time_mono; have_time = 1; - memcpy(&tv, &t->time, sizeof(tv)); break; } } lock_rw_unlock(&c->doq_socket->table->lock); - + c->doq_socket->marked_time = ts; if(have_time) { struct timeval rel; timeval_subtract(&rel, &tv, c->doq_socket->now_tv); comm_timer_set(c->doq_socket->timer, &rel); - memcpy(&c->doq_socket->marked_time, &tv, - sizeof(c->doq_socket->marked_time)); verbose(VERB_ALGO, "doq pickup timer at %d.%6.6d in %d.%6.6d", (int)tv.tv_sec, (int)tv.tv_usec, (int)rel.tv_sec, (int)rel.tv_usec); } else { if(comm_timer_is_set(c->doq_socket->timer)) comm_timer_disable(c->doq_socket->timer); - memset(&c->doq_socket->marked_time, 0, - sizeof(c->doq_socket->marked_time)); verbose(VERB_ALGO, "doq timer disabled"); } } @@ -2213,13 +2204,14 @@ doq_done_setup_timer_and_write(struct comm_point* c, struct doq_conn* conn) uint8_t cid[NGTCP2_MAX_CIDLEN]; rbnode_type* node; struct timeval new_tv; + ngtcp2_tstamp new_ts; int write_change = 0, timer_change = 0; /* No longer in callbacks, so the pointer to doq_socket is back * to NULL. */ conn->doq_socket = NULL; - if(doq_conn_check_timer(conn, &new_tv)) + if(doq_conn_check_timer(conn, &new_tv, &new_ts)) timer_change = 1; if( (conn->write_interest && !conn->on_write_list) || (!conn->write_interest && conn->on_write_list)) @@ -2265,7 +2257,7 @@ doq_done_setup_timer_and_write(struct comm_point* c, struct doq_conn* conn) } if(timer_change) { doq_timer_set(c->doq_socket->table, &conn->timer, - c->doq_socket, &new_tv); + c->doq_socket, &new_tv, new_ts); } lock_rw_unlock(&c->doq_socket->table->lock); lock_basic_unlock(&conn->lock); @@ -2429,7 +2421,7 @@ doq_write_blocked_pkt(struct comm_point* c) return 1; } -/** doq find a timer that timeouted and return the conn, locked. */ +/** doq find a timer that timed out and return the conn, locked. */ static struct doq_conn* doq_timer_timeout_conn(struct doq_server_socket* doq_socket) { @@ -2442,7 +2434,7 @@ doq_timer_timeout_conn(struct doq_server_socket* doq_socket) conn = t->conn; /* If now < timer then no further timeouts in tree. */ - if(timeval_smaller(doq_socket->now_tv, &t->time)) { + if(timeval_smaller(doq_socket->now_tv, &t->time_real)) { lock_rw_unlock(&doq_socket->table->lock); return NULL; } @@ -2465,11 +2457,11 @@ doq_timer_erase_marker(struct doq_server_socket* doq_socket) { struct doq_timer* t; lock_rw_wrlock(&doq_socket->table->lock); - t = doq_timer_find_time(doq_socket->table, &doq_socket->marked_time); + t = doq_timer_find_time(doq_socket->table, doq_socket->marked_time); if(t && t->worker_doq_socket == doq_socket) t->worker_doq_socket = NULL; lock_rw_unlock(&doq_socket->table->lock); - memset(&doq_socket->marked_time, 0, sizeof(doq_socket->marked_time)); + doq_socket->marked_time = 0; } void @@ -2776,7 +2768,7 @@ doq_server_socket_create(struct doq_table* table, struct ub_randstate* rnd, free(doq_socket); return NULL; } - memset(&doq_socket->marked_time, 0, sizeof(doq_socket->marked_time)); + doq_socket->marked_time = 0; comm_base_timept(base, &doq_socket->now_tt, &doq_socket->now_tv); doq_socket->cfg = cfg; return doq_socket; diff --git a/util/netevent.h b/util/netevent.h index c5114bbbe..7235843ee 100644 --- a/util/netevent.h +++ b/util/netevent.h @@ -1093,8 +1093,10 @@ struct doq_server_socket { struct doq_pkt_addr* blocked_paddr; /** timer for this worker on this comm_point to wait on. */ struct comm_timer* timer; +#ifdef HAVE_NGTCP2 /** the timer that is marked by the doq_socket as waited on. */ - struct timeval marked_time; + ngtcp2_tstamp marked_time; +#endif /** the current time for use by time functions, time_t. */ time_t* now_tt; /** the current time for use by time functions, timeval. */ From 01dfd2f466d383370405d8ecf939570947f3523e Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:09:26 +0200 Subject: [PATCH 04/26] - Fix CVE-2026-32665, Remote DNS-over-QUIC denial of service due to `quic-size` budget bypass. Thanks to N0zoM1z0 (https://github.com/N0zoM1z0) for the report. In addition, thanks to Kunta Chu, Kaihua Wang, and Jianjun Chen from Tsinghua University, for also reporting this issue. In addition, thanks to Qifan Zhang, Palo Alto Networks, for also reporting this issue. In addition, thanks to Xuanchao Xie, for also reporting this issue. --- services/listen_dnsport.c | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index a08a5352e..caae7fcc4 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -3964,7 +3964,8 @@ doq_stream_close(struct doq_conn* conn, struct doq_stream* stream, /** doq stream pick up answer data from buffer */ static int -doq_stream_pickup_answer(struct doq_stream* stream, struct sldns_buffer* buf) +doq_stream_pickup_answer(struct doq_conn* conn, struct doq_stream* stream, + struct sldns_buffer* buf) { stream->is_answer_available = 1; if(stream->out) { @@ -3974,6 +3975,11 @@ doq_stream_pickup_answer(struct doq_stream* stream, struct sldns_buffer* buf) } stream->nwrite = 0; stream->outlen = sldns_buffer_limit(buf); + if(!doq_table_quic_size_available(conn->doq_socket->table, + conn->doq_socket->cfg, stream->outlen)) { + verbose(VERB_ALGO, "doq stream: no space for reply length"); + return 0; + } /* For quic the output bytes have to stay allocated and available, * for potential resends, until the remote end has acknowledged them. * This includes the tcplen start uint16_t, in outlen_wire. */ @@ -4000,7 +4006,7 @@ doq_stream_send_reply(struct doq_conn* conn, struct doq_stream* stream, if(stream->out) doq_table_quic_size_subtract(conn->doq_socket->table, stream->outlen); - if(!doq_stream_pickup_answer(stream, buf)) + if(!doq_stream_pickup_answer(conn, stream, buf)) return 0; doq_table_quic_size_add(conn->doq_socket->table, stream->outlen); doq_stream_on_write_list(conn, stream); @@ -4011,13 +4017,19 @@ doq_stream_send_reply(struct doq_conn* conn, struct doq_stream* stream, /** doq stream data length has completed, allocations can be done. False on * allocation failure. */ static int -doq_stream_datalen_complete(struct doq_stream* stream, struct doq_table* table) +doq_stream_datalen_complete(struct doq_conn* conn, struct doq_stream* stream, + struct doq_table* table) { if(stream->inlen > 1024*1024) { log_err("doq stream in length too large %d", (int)stream->inlen); return 0; } + if(!doq_table_quic_size_available(table, conn->doq_socket->cfg, + stream->inlen)) { + verbose(VERB_ALGO, "doq stream: no space for query length"); + return 0; + } stream->in = calloc(1, stream->inlen); if(!stream->in) { log_err("doq could not read stream, calloc failed: " @@ -4078,8 +4090,9 @@ doq_stream_data_complete(struct doq_conn* conn, struct doq_stream* stream) /** doq receive data for a stream, more bytes of the incoming data */ static int -doq_stream_recv_data(struct doq_stream* stream, const uint8_t* data, - size_t datalen, int* recv_done, struct doq_table* table) +doq_stream_recv_data(struct doq_conn* conn, struct doq_stream* stream, + const uint8_t* data, size_t datalen, int* recv_done, + struct doq_table* table) { int got_data = 0; /* read the tcplength uint16_t at the start */ @@ -4100,7 +4113,7 @@ doq_stream_recv_data(struct doq_stream* stream, const uint8_t* data, if(stream->nread == 2) { /* the initial length value is completed */ stream->inlen = ntohs(tcplen); - if(!doq_stream_datalen_complete(stream, table)) + if(!doq_stream_datalen_complete(conn, stream, table)) return 0; } else { /* store for later */ @@ -4316,8 +4329,7 @@ doq_stream_open_cb(ngtcp2_conn* ATTR_UNUSED(conn), int64_t stream_id, verbose(VERB_ALGO, "doq: stream with this id already exists"); return 0; } - if(stream_id != 0 && stream_id != 4 && /* allow one stream on a new connection */ - !doq_table_quic_size_available(doq_conn->doq_socket->table, + if(!doq_table_quic_size_available(doq_conn->doq_socket->table, doq_conn->doq_socket->cfg, sizeof(*stream) + 100 /* estimated query in */ + 512 /* estimated response out */ @@ -4375,8 +4387,8 @@ doq_recv_stream_data_cb(ngtcp2_conn* ATTR_UNUSED(conn), uint32_t flags, return 0; } if(datalen != 0) { - if(!doq_stream_recv_data(stream, data, datalen, &recv_done, - doq_conn->doq_socket->table)) + if(!doq_stream_recv_data(doq_conn, stream, data, datalen, + &recv_done, doq_conn->doq_socket->table)) return NGTCP2_ERR_CALLBACK_FAILURE; } if((flags&NGTCP2_STREAM_DATA_FLAG_FIN)!=0) { From f54e0791ba4488284d9f6059014640a3d0bd30da Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:09:50 +0200 Subject: [PATCH 05/26] - Fix CVE-2026-40691, Packet of death for DNSCrypt over TCP. Thanks to Qifan Zhang, Palo Alto Networks, for the report. In addition, thanks to Trung Nguyen (@everping) of CyStack, for also reporting this issue. --- daemon/worker.c | 10 +++++++++- dnscrypt/dnscrypt.c | 14 ++++++++++++-- dnscrypt/dnscrypt.h | 3 ++- testdata/dnscrypt_cert.tdir/dnscrypt_cert.test | 17 +++++++++-------- .../dnscrypt_cert_chacha.test | 17 +++++++++-------- util/netevent.c | 4 +++- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/daemon/worker.c b/daemon/worker.c index a5dd9bc02..b4e50b345 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -1550,6 +1550,7 @@ worker_handle_request(struct comm_point* c, void* arg, int error, return 0; } query_error(c->buffer, LDNS_RCODE_FORMERR, 0); + sldns_buffer_copy(c->dnscrypt_buffer, c->buffer); return 1; } dname_str(qinfo.qname, buf); @@ -1568,6 +1569,7 @@ worker_handle_request(struct comm_point* c, void* arg, int error, query_error(c->buffer, LDNS_RCODE_SERVFAIL, qinfo.qname_len); worker->stats.num_query_dnscrypt_cleartext++; + sldns_buffer_copy(c->dnscrypt_buffer, c->buffer); return 1; } worker->stats.num_query_dnscrypt_cert++; @@ -1828,7 +1830,13 @@ worker_handle_request(struct comm_point* c, void* arg, int error, server_stats_insquery(&worker->stats, c, qinfo.qtype, qinfo.qclass, &edns, repinfo); if(c->type != comm_udp) +#ifdef USE_DNSCRYPT + edns.udp_size = (c->dnscrypt && repinfo->is_dnscrypted) + ? sldns_buffer_capacity(c->buffer) - DNSCRYPT_REPLY_HEADER_SIZE + : 65535; +#else edns.udp_size = 65535; /* max size for TCP replies */ +#endif if(qinfo.qclass == LDNS_RR_CLASS_CH && answer_chaos(worker, &qinfo, &edns, repinfo, c->buffer)) { regional_free_all(worker->scratchpad); @@ -2112,7 +2120,7 @@ send_reply_rc: } } #ifdef USE_DNSCRYPT - if(!dnsc_handle_uncurved_request(repinfo)) { + if(!dnsc_handle_uncurved_request(repinfo, c->buffer)) { return 0; } #endif diff --git a/dnscrypt/dnscrypt.c b/dnscrypt/dnscrypt.c index 173484cdf..08f9dcb8c 100644 --- a/dnscrypt/dnscrypt.c +++ b/dnscrypt/dnscrypt.c @@ -474,10 +474,18 @@ dnscrypt_server_curve(const dnsccert *cert, uint8_t *const buf = sldns_buffer_begin(buffer); size_t len = sldns_buffer_limit(buffer); + if(len + DNSCRYPT_REPLY_HEADER_SIZE > sldns_buffer_capacity(buffer)) + return -1; + sldns_buffer_clear(buffer); + if(udp){ if (max_len > max_reply_size) max_len = max_reply_size; } + if(max_len > sldns_buffer_capacity(buffer)) + max_len = sldns_buffer_capacity(buffer); + if(max_len > 65535) + max_len = 65535; memcpy(nonce, client_nonce, crypto_box_HALF_NONCEBYTES); @@ -520,6 +528,7 @@ dnscrypt_server_curve(const dnsccert *cert, DNSCRYPT_MAGIC_HEADER_LEN, nonce, crypto_box_NONCEBYTES); + sldns_buffer_flip(buffer); sldns_buffer_set_limit(buffer, len + DNSCRYPT_REPLY_HEADER_SIZE); return 0; } @@ -912,12 +921,13 @@ dnsc_handle_curved_request(struct dnsc_env* dnscenv, } int -dnsc_handle_uncurved_request(struct comm_reply *repinfo) +dnsc_handle_uncurved_request(struct comm_reply *repinfo, + struct sldns_buffer* buffer) { if(!repinfo->c->dnscrypt) { return 1; } - sldns_buffer_copy(repinfo->c->dnscrypt_buffer, repinfo->c->buffer); + sldns_buffer_copy(repinfo->c->dnscrypt_buffer, buffer); if(!repinfo->is_dnscrypted) { return 1; } diff --git a/dnscrypt/dnscrypt.h b/dnscrypt/dnscrypt.h index b0da9b732..998237d48 100644 --- a/dnscrypt/dnscrypt.h +++ b/dnscrypt/dnscrypt.h @@ -128,7 +128,8 @@ int dnsc_handle_curved_request(struct dnsc_env* dnscenv, * \return 0 in case of failure. */ -int dnsc_handle_uncurved_request(struct comm_reply *repinfo); +int dnsc_handle_uncurved_request(struct comm_reply *repinfo, + struct sldns_buffer* buffer); /** * Computes the size of the shared secret cache entry. diff --git a/testdata/dnscrypt_cert.tdir/dnscrypt_cert.test b/testdata/dnscrypt_cert.tdir/dnscrypt_cert.test index fdb88e8f9..4631d02e1 100644 --- a/testdata/dnscrypt_cert.tdir/dnscrypt_cert.test +++ b/testdata/dnscrypt_cert.tdir/dnscrypt_cert.test @@ -9,20 +9,21 @@ PRE="../.." # do the test -# Query plain request over DNSCrypt channel get closed -# We use TCP to avoid hanging on waiting for UDP. -# We expect `outfile` to contain no DNS payload -echo "> dig TCP www.example.com. DNSCrypt port" -dig +tcp @127.0.0.1 -p $DNSCRYPT_PORT www.example.com. A | tee outfile +# Query plain request over DNSCrypt. +# This used to close the channel; now it returns SERVFAIL. +# Old: We use TCP to avoid hanging on waiting for UDP. +# We expect `outfile` to contain no DNS payload +echo "> dig www.example.com. DNSCrypt port" +dig @127.0.0.1 -p $DNSCRYPT_PORT www.example.com. A | tee outfile echo "> cat logfiles" cat fwd.log cat unbound.log echo "> check answer" -if grep "QUESTION SECTION" outfile; then +if grep "SERVFAIL" outfile; then + echo "OK" +else echo "NOK" exit 1 -else - echo "OK" fi diff --git a/testdata/dnscrypt_cert_chacha.tdir/dnscrypt_cert_chacha.test b/testdata/dnscrypt_cert_chacha.tdir/dnscrypt_cert_chacha.test index 2db073ad6..a92f196f5 100644 --- a/testdata/dnscrypt_cert_chacha.tdir/dnscrypt_cert_chacha.test +++ b/testdata/dnscrypt_cert_chacha.tdir/dnscrypt_cert_chacha.test @@ -9,20 +9,21 @@ PRE="../.." # do the test -# Query plain request over DNSCrypt channel get closed -# We use TCP to avoid hanging on waiting for UDP. -# We expect `outfile` to contain no DNS payload -echo "> dig TCP www.example.com. DNSCrypt port" -dig +tcp @127.0.0.1 -p $DNSCRYPT_PORT www.example.com. A | tee outfile +# Query plain request over DNSCrypt. +# This used to close the channel; now it returns SERVFAIL. +# Old: We use TCP to avoid hanging on waiting for UDP. +# We expect `outfile` to contain no DNS payload +echo "> dig www.example.com. DNSCrypt port" +dig @127.0.0.1 -p $DNSCRYPT_PORT www.example.com. A | tee outfile echo "> cat logfiles" cat fwd.log cat unbound.log echo "> check answer" -if grep "QUESTION SECTION" outfile; then +if grep "SERVFAIL" outfile; then + echo "OK" +else echo "NOK" exit 1 -else - echo "OK" fi diff --git a/util/netevent.c b/util/netevent.c index e1480dc22..58938a220 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -6677,7 +6677,9 @@ comm_point_send_reply(struct comm_reply *repinfo) log_assert(repinfo && repinfo->c); #ifdef USE_DNSCRYPT buffer = repinfo->c->dnscrypt_buffer; - if(!dnsc_handle_uncurved_request(repinfo)) { + if(!dnsc_handle_uncurved_request(repinfo, + repinfo->c->tcp_req_info? + repinfo->c->tcp_req_info->spool_buffer:repinfo->c->buffer)) { return; } #else From 27f22b88081eca4faf6b51194bd5088a034fcf33 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:10:24 +0200 Subject: [PATCH 06/26] - Fix CVE-2026-41637, Degradation of resolution service from improperly accounted client-terminated DNS-over-QUIC queries. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/listen_dnsport.c | 57 ++++++++++++++++++++++++++++++++++++--- services/listen_dnsport.h | 16 +++++++++++ services/mesh.c | 29 ++++++++++++++++++-- services/mesh.h | 4 ++- testcode/fake_event.c | 9 +++++++ util/netevent.c | 2 +- util/netevent.h | 2 ++ 7 files changed, 112 insertions(+), 7 deletions(-) diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index caae7fcc4..0500bff8d 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -2166,7 +2166,8 @@ void tcp_req_info_clear(struct tcp_req_info* req) open = req->open_req_list; while(open) { nopen = open->next; - mesh_state_remove_reply(open->mesh, open->mesh_state, req->cp); + mesh_state_remove_reply(open->mesh, open->mesh_state, req->cp, + NULL); free(open); open = nopen; } @@ -3596,15 +3597,29 @@ doq_conn_create(struct comm_point* c, struct doq_pkt_addr* paddr, return conn; } +/** The arguments for doq stream tree del. */ +struct doq_stream_tree_del_args { + /** The doq table. */ + struct doq_table* table; + /** The doq connection for the stream. */ + struct doq_conn* conn; +}; + /** delete stream tree node */ static void stream_tree_del(rbnode_type* node, void* arg) { - struct doq_table* table = (struct doq_table*)arg; + struct doq_stream_tree_del_args* args = (struct doq_stream_tree_del_args*)arg; + struct doq_table* table = args->table; struct doq_stream* stream; if(!node) return; stream = (struct doq_stream*)node; + if(stream->mesh_state) { + mesh_state_remove_reply(stream->mesh, stream->mesh_state, + args->conn->doq_socket->cp, stream); + stream->mesh_state = NULL; + } if(stream->in) doq_table_quic_size_subtract(table, stream->inlen); if(stream->out) @@ -3626,7 +3641,11 @@ doq_conn_delete(struct doq_conn* conn, struct doq_table* table) * because the ngtcp2 conn is deleted. */ SSL_set_app_data(conn->ssl, NULL); if(conn->stream_tree.count != 0) { - traverse_postorder(&conn->stream_tree, stream_tree_del, table); + struct doq_stream_tree_del_args args; + memset(&args, 0, sizeof(args)); + args.table = table; + args.conn = conn; + traverse_postorder(&conn->stream_tree, stream_tree_del, &args); } free(conn->key.dcid); SSL_free(conn->ssl); @@ -3935,6 +3954,11 @@ doq_stream_close(struct doq_conn* conn, struct doq_stream* stream, if(stream->is_closed) return 1; stream->is_closed = 1; + if(stream->mesh_state) { + mesh_state_remove_reply(stream->mesh, stream->mesh_state, + conn->doq_socket->cp, stream); + stream->mesh_state = NULL; + } doq_stream_off_write_list(conn, stream); if(send_shutdown) { verbose(VERB_ALGO, "doq: shutdown stream_id %d with app_error_code %d", @@ -4013,7 +4037,33 @@ doq_stream_send_reply(struct doq_conn* conn, struct doq_stream* stream, doq_conn_write_enable(conn); return 1; } +#endif /* HAVE_NGTCP2 */ +void +doq_stream_add_meshstate(struct doq_stream* stream, + struct mesh_area* mesh, struct mesh_state* m) +{ +#ifdef HAVE_NGTCP2 + stream->mesh = mesh; + stream->mesh_state = m; +#else + (void)stream; (void)mesh; (void)m; +#endif +} + +void +doq_stream_remove_mesh_state(struct doq_stream* stream) +{ +#ifdef HAVE_NGTCP2 + if(!stream) + return; + stream->mesh_state = NULL; +#else + (void)stream; +#endif +} + +#ifdef HAVE_NGTCP2 /** doq stream data length has completed, allocations can be done. False on * allocation failure. */ static int @@ -4074,6 +4124,7 @@ doq_stream_data_complete(struct doq_conn* conn, struct doq_stream* stream) return 0; } c->repinfo.doq_streamid = stream->stream_id; + c->repinfo.doq_stream = stream; conn->doq_socket->current_conn = conn; fptr_ok(fptr_whitelist_comm_point(c->callback)); if( (*c->callback)(c, c->cb_arg, NETEVENT_NOERROR, &c->repinfo)) { diff --git a/services/listen_dnsport.h b/services/listen_dnsport.h index 95aa3e11e..ae0463468 100644 --- a/services/listen_dnsport.h +++ b/services/listen_dnsport.h @@ -61,6 +61,8 @@ struct config_file; struct addrinfo; struct sldns_buffer; struct tcl_list; +struct mesh_area; +struct mesh_state; /** * Listening for queries structure. @@ -692,6 +694,11 @@ struct doq_stream { uint8_t* out; /** if the stream is on the write list */ uint8_t on_write_list; + /** The mesh area and mesh state, set when this stream's query was + * dispatched into the mesh; used to detach the reply on stream close */ + struct mesh_area* mesh; + /** the mesh state for the query, is nonNULL when there is one. */ + struct mesh_state* mesh_state; /** the prev and next on the write list, if on the list */ struct doq_stream* write_prev, *write_next; }; @@ -794,7 +801,16 @@ int doq_stream_close(struct doq_conn* conn, struct doq_stream* stream, /** send reply for a connection */ int doq_stream_send_reply(struct doq_conn* conn, struct doq_stream* stream, struct sldns_buffer* buf); +#endif /* HAVE_NGTCP2 */ +/** add mesh state to doq stream */ +void doq_stream_add_meshstate(struct doq_stream* stream, + struct mesh_area* mesh, struct mesh_state* m); + +/** remove mesh state from doq stream */ +void doq_stream_remove_mesh_state(struct doq_stream* stream); + +#ifdef HAVE_NGTCP2 /** the connection has write interest, wants to write packets */ void doq_conn_write_enable(struct doq_conn* conn); diff --git a/services/mesh.c b/services/mesh.c index 286901047..f06c1cb9d 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -467,6 +467,8 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo, "incoming query."); if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); + else if(rep->c->type == comm_doq && rep->doq_stream) + doq_stream_remove_mesh_state(rep->doq_stream); comm_point_drop_reply(rep); mesh->stats_dropped++; return; @@ -480,6 +482,8 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo, "dropping incoming query."); if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); + else if(rep->c->type == comm_doq && rep->doq_stream) + doq_stream_remove_mesh_state(rep->doq_stream); comm_point_drop_reply(rep); mesh->num_queries_replyaddr_limit++; return; @@ -552,6 +556,8 @@ void mesh_new_client(struct mesh_area* mesh, struct query_info* qinfo, } if(rep->c->use_h2) { http2_stream_add_meshstate(rep->c->h2_stream, mesh, s); + } else if(rep->c->type == comm_doq && rep->doq_stream) { + doq_stream_add_meshstate(rep->doq_stream, mesh, s); } /* add serve expired timer if required and not already there */ if(timeout && !mesh_serve_expired_init(s, timeout)) { @@ -605,6 +611,8 @@ servfail_mem: qinfo, qid, qflags, edns); if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); + else if(rep->c->type == comm_doq && rep->doq_stream) + doq_stream_remove_mesh_state(rep->doq_stream); comm_point_send_reply(rep); if(added) mesh_state_delete(&s->s); @@ -1484,6 +1492,10 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, * for HTTP/2 stream to refer to mesh state, in case * connection gets cleanup before HTTP/2 stream close. */ r->h2_stream->mesh_state = NULL; +#ifdef HAVE_NGTCP2 + } else if(r->query_reply.doq_stream) { + r->query_reply.doq_stream->mesh_state = NULL; +#endif } /* send the reply */ /* We don't reuse the encoded answer if: @@ -1777,6 +1789,8 @@ void mesh_query_done(struct mesh_state* mstate) mstate->reply_list = NULL; 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); @@ -1814,6 +1828,8 @@ void mesh_query_done(struct mesh_state* mstate) mstate->reply_list = NULL; 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; @@ -2009,6 +2025,8 @@ int mesh_state_add_reply(struct mesh_state* s, struct edns_data* edns, if(rep->c->use_h2) r->h2_stream = rep->c->h2_stream; else r->h2_stream = NULL; + if(rep->c->type != comm_doq) + r->query_reply.doq_stream = NULL; /* Data related to local alias stored in 'qinfo' (if any) is ephemeral * and can be different for different original queries (even if the @@ -2366,7 +2384,7 @@ void mesh_list_remove(struct mesh_state* m, struct mesh_state** fp, } void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, - struct comm_point* cp) + struct comm_point* cp, struct doq_stream* doq_stream) { struct mesh_reply* n, *prev = NULL; n = m->reply_list; @@ -2374,7 +2392,8 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, * there is no accounting twice */ if(!n) return; /* nothing to remove, also no accounting needed */ while(n) { - if(n->query_reply.c == cp) { + if(n->query_reply.c == cp + && (!doq_stream || n->query_reply.doq_stream == doq_stream)) { /* unlink it */ if(prev) prev->next = n->next; else m->reply_list = n->next; @@ -2387,6 +2406,10 @@ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, * share the same comm_point); make sure the streams * don't point back. */ if(n->h2_stream) n->h2_stream->mesh_state = NULL; +#ifdef HAVE_NGTCP2 + if(n->query_reply.doq_stream) + n->query_reply.doq_stream->mesh_state = NULL; +#endif /* prev = prev; */ n = n->next; @@ -2554,6 +2577,8 @@ mesh_serve_expired_callback(void* arg) mstate->reply_list = NULL; 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; mstate->s.env->mesh->num_queries_discard_timeout++; diff --git a/services/mesh.h b/services/mesh.h index 9ee585156..352260e26 100644 --- a/services/mesh.h +++ b/services/mesh.h @@ -683,9 +683,11 @@ void mesh_list_remove(struct mesh_state* m, struct mesh_state** fp, * @param mesh: to update the counters. * @param m: the mesh state. * @param cp: the comm_point to remove from the list. + * @param doq_stream: if not NULL, it specifies the doq_stream to match + * for the delete. */ void mesh_state_remove_reply(struct mesh_area* mesh, struct mesh_state* m, - struct comm_point* cp); + struct comm_point* cp, struct doq_stream* doq_stream); /** Callback for when the serve expired client timer has run out. Tries to * find an expired answer in the cache and reply that to the client. diff --git a/testcode/fake_event.c b/testcode/fake_event.c index ce439edd1..b127d0df8 100644 --- a/testcode/fake_event.c +++ b/testcode/fake_event.c @@ -2021,6 +2021,15 @@ void http2_stream_remove_mesh_state(struct http2_stream* ATTR_UNUSED(h2_stream)) { } +void doq_stream_add_meshstate(struct doq_stream* ATTR_UNUSED(stream), + struct mesh_area* ATTR_UNUSED(mesh), struct mesh_state* ATTR_UNUSED(m)) +{ +} + +void doq_stream_remove_mesh_state(struct doq_stream* ATTR_UNUSED(stream)) +{ +} + void fast_reload_service_cb(int ATTR_UNUSED(fd), short ATTR_UNUSED(event), void* ATTR_UNUSED(arg)) { diff --git a/util/netevent.c b/util/netevent.c index 58938a220..b7092b88a 100644 --- a/util/netevent.c +++ b/util/netevent.c @@ -3166,7 +3166,7 @@ static void http2_stream_delete(struct http2_session* h2_session, { if(h2_stream->mesh_state) { mesh_state_remove_reply(h2_stream->mesh, h2_stream->mesh_state, - h2_session->c); + h2_session->c, NULL); h2_stream->mesh_state = NULL; } http2_req_stream_clear(h2_stream); diff --git a/util/netevent.h b/util/netevent.h index 7235843ee..aee8a6539 100644 --- a/util/netevent.h +++ b/util/netevent.h @@ -187,6 +187,8 @@ struct comm_reply { /** port number for doq */ int doq_srcport; #endif /* HAVE_NGTCP2 */ + /** The doq stream to register mesh states to. */ + struct doq_stream* doq_stream; }; /** From 13ec8d0f261ee7900ac67cfece551e8a703d14b1 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:11:04 +0200 Subject: [PATCH 07/26] - Fix CVE-2026-42955, Extra fix for CVE-2026-40622 to also clamp the TTL of A/AAAA records disallowing a one-time 'ghost domain' delegation renewal via glue records. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/cache/rrset.c | 12 +++++++++--- testdata/iter_prefetch_fail.rpl | 8 ++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/services/cache/rrset.c b/services/cache/rrset.c index ab4f4c8e0..a5d320a30 100644 --- a/services/cache/rrset.c +++ b/services/cache/rrset.c @@ -126,7 +126,8 @@ rrset_cache_touch(struct rrset_cache* r, struct ub_packed_rrset_key* key, /** see if rrset needs to be updated in the cache */ static int -need_to_update_rrset(void* nd, void* cd, time_t timenow, int equal, int ns) +need_to_update_rrset(void* nd, void* cd, time_t timenow, int equal, int ns, + int a_aaaa) { struct packed_rrset_data* newd = (struct packed_rrset_data*)nd; struct packed_rrset_data* cached = (struct packed_rrset_data*)cd; @@ -151,9 +152,13 @@ need_to_update_rrset(void* nd, void* cd, time_t timenow, int equal, int ns) return 0; /* ghost-domain: never let an NS overwrite extend lifetime * past the entry it replaces, regardless of trust. */ - if(ns && !TTL_IS_EXPIRED(cached->ttl, timenow) && + /* Also for A/AAAA and it is glue. */ + if((ns || + (a_aaaa && cached->trust==rrset_trust_add_noAA)) + && !TTL_IS_EXPIRED(cached->ttl, timenow) && newd->ttl > cached->ttl) { size_t i; + if(a_aaaa) newd->trust=rrset_trust_add_noAA; newd->ttl = cached->ttl; for(i=0; i<(newd->count+newd->rrsig_count); i++) if(newd->rr_ttl[i] > newd->ttl) @@ -223,7 +228,8 @@ rrset_cache_update(struct rrset_cache* r, struct rrset_ref* ref, equal = rrsetdata_equal((struct packed_rrset_data*)k->entry. data, (struct packed_rrset_data*)e->data); if(!need_to_update_rrset(k->entry.data, e->data, timenow, - equal, (rrset_type==LDNS_RR_TYPE_NS))) { + equal, (rrset_type==LDNS_RR_TYPE_NS), + (rrset_type==LDNS_RR_TYPE_A || rrset_type==LDNS_RR_TYPE_AAAA))) { /* cache is superior, return that value */ lock_rw_unlock(&e->lock); ub_packed_rrset_parsedelete(k, alloc); diff --git a/testdata/iter_prefetch_fail.rpl b/testdata/iter_prefetch_fail.rpl index d1e308305..aa94d0fe5 100644 --- a/testdata/iter_prefetch_fail.rpl +++ b/testdata/iter_prefetch_fail.rpl @@ -319,7 +319,7 @@ example.com. 360 IN NS ns.example.com. SECTION ADDITIONAL ; this is picked up from the parent (because this simulation has the ; parent respond with servfail, not actually timeout) -ns.example.com. 3600 IN A 1.2.3.4 +ns.example.com. 360 IN A 1.2.3.4 ENTRY_END ; another query to see if there is another lookup towards the authority @@ -342,7 +342,7 @@ www.example.com. 360 IN A 10.20.30.40 SECTION AUTHORITY example.com. 360 IN NS ns.example.com. SECTION ADDITIONAL -ns.example.com. 3600 IN A 1.2.3.4 +ns.example.com. 360 IN A 1.2.3.4 ENTRY_END ; some time later another query, and now it is fine to bother the authority @@ -367,7 +367,7 @@ www.example.com. 330 IN A 10.20.30.40 SECTION AUTHORITY example.com. 330 IN NS ns.example.com. SECTION ADDITIONAL -ns.example.com. 3570 IN A 1.2.3.4 +ns.example.com. 330 IN A 1.2.3.4 ENTRY_END ; now the just-looked-up entry STEP 190 QUERY @@ -388,7 +388,7 @@ www.example.com. 3600 IN A 10.20.30.40 SECTION AUTHORITY example.com. 3600 IN NS ns.example.com. SECTION ADDITIONAL -ns.example.com. 3570 IN A 1.2.3.4 +ns.example.com. 3600 IN A 1.2.3.4 ENTRY_END From f52a9e864bfa0f10d8816b64130586b1d224c474 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:11:26 +0200 Subject: [PATCH 08/26] - Fix CVE-2026-44621, Libunbound applications configured with 'unwanted-reply-threshold' could eventually be abruptly terminated. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- daemon/worker.c | 5 +++++ dnstap/unbound-dnstap-socket.c | 5 +++++ smallapp/worker_cb.c | 6 ++++++ testcode/doqclient.c | 5 +++++ util/fptr_wlist.c | 1 + 5 files changed, 22 insertions(+) diff --git a/daemon/worker.c b/daemon/worker.c index b4e50b345..1d61f8733 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -2646,6 +2646,11 @@ void libworker_event_done_cb(void* ATTR_UNUSED(arg), int ATTR_UNUSED(rcode), log_assert(0); } +void libworker_alloc_cleanup(void* ATTR_UNUSED(arg)) +{ + log_assert(0); +} + int context_query_cmp(const void* ATTR_UNUSED(a), const void* ATTR_UNUSED(b)) { log_assert(0); diff --git a/dnstap/unbound-dnstap-socket.c b/dnstap/unbound-dnstap-socket.c index 90b0f6003..2bf017430 100644 --- a/dnstap/unbound-dnstap-socket.c +++ b/dnstap/unbound-dnstap-socket.c @@ -1735,6 +1735,11 @@ void libworker_event_done_cb(void* ATTR_UNUSED(arg), int ATTR_UNUSED(rcode), log_assert(0); } +void libworker_alloc_cleanup(void* ATTR_UNUSED(arg)) +{ + log_assert(0); +} + int context_query_cmp(const void* ATTR_UNUSED(a), const void* ATTR_UNUSED(b)) { log_assert(0); diff --git a/smallapp/worker_cb.c b/smallapp/worker_cb.c index 92ebe386d..876c7db4e 100644 --- a/smallapp/worker_cb.c +++ b/smallapp/worker_cb.c @@ -128,6 +128,12 @@ worker_alloc_cleanup(void* ATTR_UNUSED(arg)) log_assert(0); } +void +libworker_alloc_cleanup(void* ATTR_UNUSED(arg)) +{ + log_assert(0); +} + struct outbound_entry* libworker_send_query( struct query_info* ATTR_UNUSED(qinfo), uint16_t ATTR_UNUSED(flags), int ATTR_UNUSED(dnssec), int ATTR_UNUSED(want_dnssec), diff --git a/testcode/doqclient.c b/testcode/doqclient.c index 8a34ca31b..ce4d3417d 100644 --- a/testcode/doqclient.c +++ b/testcode/doqclient.c @@ -2671,6 +2671,11 @@ void libworker_event_done_cb(void* ATTR_UNUSED(arg), int ATTR_UNUSED(rcode), log_assert(0); } +void libworker_alloc_cleanup(void* ATTR_UNUSED(arg)) +{ + log_assert(0); +} + int context_query_cmp(const void* ATTR_UNUSED(a), const void* ATTR_UNUSED(b)) { log_assert(0); diff --git a/util/fptr_wlist.c b/util/fptr_wlist.c index a45134065..5edd8adf2 100644 --- a/util/fptr_wlist.c +++ b/util/fptr_wlist.c @@ -610,6 +610,7 @@ int fptr_whitelist_alloc_cleanup(void (*fptr)(void*)) { if(fptr == &worker_alloc_cleanup) return 1; + else if(fptr == &libworker_alloc_cleanup) return 1; return 0; } From 1e1940383ab5ee655fe7fac0da606fe59c76ce86 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:12:03 +0200 Subject: [PATCH 09/26] - Fix CVE-2026-44687, Off-by-one error in 'harden-below-nxdomain' logic can shadow a stub/forward zone by a legitimate parent's NXDOMAIN. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/cache/dns.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/cache/dns.c b/services/cache/dns.c index f6ce272a5..98d07bc37 100644 --- a/services/cache/dns.c +++ b/services/cache/dns.c @@ -1065,7 +1065,7 @@ dns_cache_lookup(struct module_env* env, if(env->cfg->harden_below_nxdomain) { while(!dname_is_root(k.qname)) { if(dpname && dpnamelen - && !dname_subdomain_c(k.qname, dpname)) + && !dname_strict_subdomain_c(k.qname, dpname)) break; /* no synth nxdomain above the stub */ dname_remove_label(&k.qname, &k.qname_len); h = query_info_hash(&k, flags); From f7637a4f1811f4a9331707d8a95cf0af65a97c0f Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:12:38 +0200 Subject: [PATCH 10/26] - Fix CVE-2026-44690, Cross-zone wildcard cache poisoning via RRSIG.labels manipulation. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/cache/rrset.c | 32 ++++++++++++++++++++++++++++++++ validator/val_sigcrypt.c | 7 +++++++ validator/val_utils.c | 2 +- validator/val_utils.h | 4 ++++ validator/validator.c | 15 +++++++++++++-- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/services/cache/rrset.c b/services/cache/rrset.c index a5d320a30..79bf473bb 100644 --- a/services/cache/rrset.c +++ b/services/cache/rrset.c @@ -50,6 +50,7 @@ #include "util/regional.h" #include "util/alloc.h" #include "util/net_help.h" +#include "validator/val_utils.h" void rrset_markdel(void* key) @@ -261,12 +262,43 @@ rrset_cache_update(struct rrset_cache* r, struct rrset_ref* ref, return 0; } +/** See if the name is a within signer authority */ +static int +dname_subdomain_rrsig_signers(uint8_t* dname, + struct ub_packed_rrset_key* rrset) +{ + struct packed_rrset_data* d = (struct packed_rrset_data*) + rrset->entry.data; + size_t i; + if(!d || !d->rrsig_count) + return 0; + for(i=0; irrsig_count; i++) { + uint8_t* sname = NULL; + size_t slen = 0; + rrsig_get_signer(d->rr_data[d->count+i], d->rr_len[d->count+i], + &sname, &slen); + if(!sname || !slen) + return 0; /* malformed */ + if(!dname_subdomain_c(dname, sname)) + return 0; /* not a subdomain */ + } + return 1; +} + void rrset_cache_update_wildcard(struct rrset_cache* rrset_cache, struct ub_packed_rrset_key* rrset, uint8_t* ce, size_t ce_len, struct alloc_cache* alloc, time_t timenow) { struct rrset_ref ref; uint8_t wc_dname[LDNS_MAX_DOMAINLEN+3]; + + /* See if the RRSIG signer name allows this wildcard, + * the new rrset should fall within the zone of the RRSIG signer(s). */ + if(!dname_subdomain_rrsig_signers(ce, rrset)) { + verbose(VERB_ALGO, "wildcard canonical parent outside signer authority"); + return; + } + rrset = packed_rrset_copy_alloc(rrset, alloc, timenow); if(!rrset) { log_err("malloc failure in rrset_cache_update_wildcard"); diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 9f27f9cc9..46e6ac16b 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -1666,6 +1666,13 @@ dnskey_verify_rrset_sig(struct regional* region, sldns_buffer* buf, *reason_bogus = LDNS_EDE_DNSSEC_BOGUS; return sec_status_bogus; } + if((int)sig[2+3] < dname_signame_label_count(signer)) { + verbose(VERB_QUERY, "verify: RRSIG label count too low for signer"); + *reason = "signature labelcount lower than signature signer"; + if(reason_bogus) + *reason_bogus = LDNS_EDE_DNSSEC_BOGUS; + return sec_status_bogus; + } /* original ttl, always ok */ diff --git a/validator/val_utils.c b/validator/val_utils.c index 405cf897f..6754a8bab 100644 --- a/validator/val_utils.c +++ b/validator/val_utils.c @@ -157,7 +157,7 @@ val_classify_response(uint16_t query_flags, struct query_info* origqinf, } /** Get signer name from RRSIG */ -static void +void rrsig_get_signer(uint8_t* data, size_t len, uint8_t** sname, size_t* slen) { /* RRSIG rdata is not allowed to be compressed, it is stored diff --git a/validator/val_utils.h b/validator/val_utils.h index e0c649902..f3750742b 100644 --- a/validator/val_utils.h +++ b/validator/val_utils.h @@ -438,4 +438,8 @@ struct dns_msg* val_find_DS(struct module_env* env, uint8_t* nm, size_t nmlen, int derive_cname_from_dname(struct ub_packed_rrset_key* cname, struct ub_packed_rrset_key* dname, uint8_t* out, size_t outlen); +/** Get signer name from RRSIG, sname is NULL if malformed. */ +void rrsig_get_signer(uint8_t* data, size_t len, uint8_t** sname, + size_t* slen); + #endif /* VALIDATOR_VAL_UTILS_H */ diff --git a/validator/validator.c b/validator/validator.c index 8fc9ffc94..2099d9c19 100644 --- a/validator/validator.c +++ b/validator/validator.c @@ -1043,6 +1043,10 @@ validate_positive_response(struct module_env* env, struct val_env* ve, uint8_t* wc = NULL; size_t wl; int wc_cached = 0; + int wc_to_cache = 0; + uint8_t* cache_wc = NULL; + size_t cache_wl = 0; + struct ub_packed_rrset_key* cache_s = NULL; int wc_NSEC_ok = 0; /* This is used to update the RRset cache, with the combination * of the dname expansion and this wildcard, for security status. */ @@ -1071,8 +1075,11 @@ validate_positive_response(struct module_env* env, struct val_env* ve, return; } if(wc && !wc_cached && env->cfg->aggressive_nsec) { - rrset_cache_update_wildcard(env->rrset_cache, s, wc, wl, - env->alloc, *env->now); + /* Postpone cache adjust until proof has succeeded. */ + wc_to_cache = 1; + cache_wc = wc; + cache_wl = wl; + cache_s = s; wc_cached = 1; } if(wc) wc_rrset = s; @@ -1137,6 +1144,10 @@ validate_positive_response(struct module_env* env, struct val_env* ve, entry.data)->security = sec_status_bogus; return; } + if(wc_to_cache) { + rrset_cache_update_wildcard(env->rrset_cache, cache_s, + cache_wc, cache_wl, env->alloc, *env->now); + } verbose(VERB_ALGO, "Successfully validated positive response"); chase_reply->security = sec_status_secure; From 364ac737f713b2a606f0fddecddb675d72a6a991 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:13:14 +0200 Subject: [PATCH 11/26] - Fix CVE-2026-50045, 'max-global-quota' reset by DNSSEC validation restarts. Thanks to Kunjie Shang, University of Science and Technology of China, for the report. --- iterator/iterator.c | 67 ++++++++++++++++++++++++++++++------------- util/module.h | 6 ++++ validator/validator.c | 13 +++++++++ 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/iterator/iterator.c b/iterator/iterator.c index cc38348b3..78cd9485b 100644 --- a/iterator/iterator.c +++ b/iterator/iterator.c @@ -81,7 +81,8 @@ int BLACKLIST_PENALTY = (120000*4); /** Timeout when only a single probe query per IP is allowed. */ int PROBE_MAXRTO = PROBE_MAXRTO_DEFAULT; /* in msec */ -static void target_count_increase_nx(struct iter_qstate* iq, int num); +static void target_count_increase_nx(struct module_qstate* qstate, + struct iter_qstate* iq, int num); int iter_init(struct module_env* env, int id) @@ -250,7 +251,7 @@ error_supers(struct module_qstate* qstate, int id, struct module_qstate* super) if((dpns->got4 == 2 || (!ie->supports_ipv4 && !ie->nat64.use_nat64)) && (dpns->got6 == 2 || !ie->supports_ipv6)) { dpns->resolved = 1; /* mark as failed */ - target_count_increase_nx(super_iq, 1); + target_count_increase_nx(super, super_iq, 1); } } if(qstate->qinfo.qtype == LDNS_RR_TYPE_NS) { @@ -734,7 +735,7 @@ is_caps_whitelisted(struct iter_env* ie, struct iter_qstate* iq) * created for the parent query. */ static void -target_count_create(struct iter_qstate* iq) +target_count_create(struct module_qstate* qstate, struct iter_qstate* iq) { if(!iq->target_count) { iq->target_count = (int*)calloc(TARGET_COUNT_MAX, sizeof(int)); @@ -742,33 +743,57 @@ target_count_create(struct iter_qstate* iq) if(iq->target_count) { iq->target_count[TARGET_COUNT_REF] = 1; iq->nxns_dp = (uint8_t**)calloc(1, sizeof(uint8_t*)); + /* continue global quota from where it was. */ + if(qstate->global_quota_reached > + iq->target_count[TARGET_COUNT_GLOBAL_QUOTA]) + iq->target_count[TARGET_COUNT_GLOBAL_QUOTA] = + qstate->global_quota_reached; } } } static void -target_count_increase(struct iter_qstate* iq, int num) +target_count_store(struct module_qstate* qstate, struct iter_qstate* iq) { - target_count_create(iq); + if(iq->target_count) { + /* By storing the global quota counter, it stays + * there to be picked up if the module is restarted, + * eg. due to a validator retry, and then the + * target_count_create routine picks it up. */ + if(iq->target_count[TARGET_COUNT_GLOBAL_QUOTA] > + qstate->global_quota_reached) + qstate->global_quota_reached = + iq->target_count[TARGET_COUNT_GLOBAL_QUOTA]; + } +} + +static void +target_count_increase(struct module_qstate* qstate, + struct iter_qstate* iq, int num) +{ + target_count_create(qstate, iq); if(iq->target_count) iq->target_count[TARGET_COUNT_QUERIES] += num; iq->dp_target_count++; } static void -target_count_increase_nx(struct iter_qstate* iq, int num) +target_count_increase_nx(struct module_qstate* qstate, + struct iter_qstate* iq, int num) { - target_count_create(iq); + target_count_create(qstate, iq); if(iq->target_count) iq->target_count[TARGET_COUNT_NX] += num; } static void -target_count_increase_global_quota(struct iter_qstate* iq, int num) +target_count_increase_global_quota(struct module_qstate* qstate, + struct iter_qstate* iq, int num) { - target_count_create(iq); + target_count_create(qstate, iq); if(iq->target_count) iq->target_count[TARGET_COUNT_GLOBAL_QUOTA] += num; + target_count_store(qstate, iq); } /** @@ -861,7 +886,7 @@ generate_sub_request(uint8_t* qname, size_t qnamelen, uint16_t qtype, subiq = (struct iter_qstate*)subq->minfo[id]; memset(subiq, 0, sizeof(*subiq)); subiq->num_target_queries = 0; - target_count_create(iq); + target_count_create(qstate, iq); subiq->target_count = iq->target_count; if(iq->target_count) { iq->target_count[TARGET_COUNT_REF] ++; /* extra reference */ @@ -2234,7 +2259,7 @@ processLastResort(struct module_qstate* qstate, struct iter_qstate* iq, return error_response_cache(qstate, id, LDNS_RCODE_SERVFAIL); } iq->num_target_queries += qs; - target_count_increase(iq, qs); + target_count_increase(qstate, iq, qs); if(qs != 0) { qstate->ext_state[id] = module_wait_subquery; return 0; /* and wait for them */ @@ -2290,7 +2315,7 @@ processLastResort(struct module_qstate* qstate, struct iter_qstate* iq, * lookups at a time. */ verbose(VERB_ALGO, "try parent-side glue lookup"); iq->num_target_queries += query_count; - target_count_increase(iq, query_count); + target_count_increase(qstate, iq, query_count); qstate->ext_state[id] = module_wait_subquery; return 0; } @@ -2310,7 +2335,7 @@ processLastResort(struct module_qstate* qstate, struct iter_qstate* iq, if(query_count != 0) { /* suspend to await results */ verbose(VERB_ALGO, "try parent-side glue lookup"); iq->num_target_queries += query_count; - target_count_increase(iq, query_count); + target_count_increase(qstate, iq, query_count); qstate->ext_state[id] = module_wait_subquery; return 0; } @@ -2788,7 +2813,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq, return error_response_cache(qstate, id, LDNS_RCODE_SERVFAIL); } iq->num_target_queries += extra; - target_count_increase(iq, extra); + target_count_increase(qstate, iq, extra); if(iq->num_target_queries > 0) { /* wait to get all targets, we want to try em */ verbose(VERB_ALGO, "wait for all targets for fallback"); @@ -2839,7 +2864,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq, /* errors ignored, these targets are not strictly necessary for * this result, we do not have to reply with SERVFAIL */ iq->num_target_queries += extra; - target_count_increase(iq, extra); + target_count_increase(qstate, iq, extra); } /* Add the current set of unused targets to our queue. */ @@ -2962,7 +2987,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq, LDNS_RCODE_SERVFAIL); } iq->num_target_queries += qs; - target_count_increase(iq, qs); + target_count_increase(qstate, iq, qs); } /* Since a target query might have been made, we * need to check again. */ @@ -3022,7 +3047,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq, * this result, we do not have to reply with SERVFAIL */ if(extra > 0) { iq->num_target_queries += extra; - target_count_increase(iq, extra); + target_count_increase(qstate, iq, extra); check_waiting_queries(iq, qstate, id); /* undo qname minimise step because we'll get back here * to do it again */ @@ -3035,7 +3060,7 @@ processQueryTargets(struct module_qstate* qstate, struct iter_qstate* iq, } } - target_count_increase_global_quota(iq, 1); + target_count_increase_global_quota(qstate, iq, 1); if(iq->target_count && iq->target_count[TARGET_COUNT_GLOBAL_QUOTA] > MAX_GLOBAL_QUOTA) { char s[LDNS_MAX_DOMAINLEN]; @@ -3879,7 +3904,7 @@ processTargetResponse(struct module_qstate* qstate, int id, /* no new addresses, increase the nxns counter, like * this could be a list of wildcards with no new * addresses */ - target_count_increase_nx(foriq, 1); + target_count_increase_nx(qstate, foriq, 1); } verbose(VERB_ALGO, "added target response"); delegpt_log(VERB_ALGO, foriq->dp); @@ -3891,7 +3916,7 @@ processTargetResponse(struct module_qstate* qstate, int id, dpns->resolved = 1; /* fail the target */ /* do not count cached answers */ if(qstate->reply_origin && qstate->reply_origin->len != 0) { - target_count_increase_nx(foriq, 1); + target_count_increase_nx(qstate, foriq, 1); } } } @@ -4116,6 +4141,7 @@ processFinished(struct module_qstate* qstate, struct iter_qstate* iq, iter_store_parentside_neg(qstate->env, &qstate->qinfo, iq->deleg_msg?iq->deleg_msg->rep: (iq->response?iq->response->rep:NULL)); + target_count_store(qstate, iq); if(!iq->response) { verbose(VERB_ALGO, "No response is set, servfail"); errinf(qstate, "(no response found at query finish)"); @@ -4531,6 +4557,7 @@ iter_clear(struct module_qstate* qstate, int id) iq = (struct iter_qstate*)qstate->minfo[id]; if(iq) { outbound_list_clear(&iq->outlist); + target_count_store(qstate, iq); if(iq->target_count && --iq->target_count[TARGET_COUNT_REF] == 0) { free(iq->target_count); if(*iq->nxns_dp) free(*iq->nxns_dp); diff --git a/util/module.h b/util/module.h index a6fa6be90..75c3675e1 100644 --- a/util/module.h +++ b/util/module.h @@ -721,6 +721,12 @@ struct module_qstate { /** whether the reply should be dropped */ int is_drop; + /** the global quota that was reached, by one of the modules. + * So that continued counting can go on from that point. */ + int global_quota_reached; + /** the global quota that a query started with, it is a subquery, + * so that calling mesh states can see the increase. */ + int global_quota_started; }; /** diff --git a/validator/validator.c b/validator/validator.c index 2099d9c19..f27ae5b42 100644 --- a/validator/validator.c +++ b/validator/validator.c @@ -517,6 +517,14 @@ generate_request(struct module_qstate* qstate, int id, uint8_t* name, /* add our blacklist to the query blacklist */ sock_list_merge(&(*newq)->blacklist, (*newq)->region, vq->chain_blacklist); + /* start its global quota counter where this one is. */ + if(qstate->global_quota_reached > + (*newq)->global_quota_reached) { + (*newq)->global_quota_started = + qstate->global_quota_reached; + (*newq)->global_quota_reached = + qstate->global_quota_reached; + } } qstate->ext_state[id] = module_wait_subquery; return 1; @@ -3576,6 +3584,11 @@ val_inform_super(struct module_qstate* qstate, int id, verbose(VERB_ALGO, "super: has no validator state"); return; } + /* Pick up the global quota limit from the subquery. */ + if(qstate->global_quota_reached > qstate->global_quota_started) { + super->global_quota_reached += qstate->global_quota_reached - + qstate->global_quota_started; + } if(vq->wait_prime_ta) { vq->wait_prime_ta = 0; process_prime_response(super, vq, id, qstate->return_rcode, From 1ad8d4c39594dcb28d636fb4922737a3640c9a65 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:13:36 +0200 Subject: [PATCH 12/26] - Fix CVE-2026-50046, Possible heap use-after-free in an error path when a DoT forwarded query is jostled out. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/outside_network.c | 12 +++++++++++- services/outside_network.h | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/services/outside_network.c b/services/outside_network.c index 8034ff60b..0fff18666 100644 --- a/services/outside_network.c +++ b/services/outside_network.c @@ -208,6 +208,7 @@ static void waiting_tcp_delete(struct waiting_tcp* w) { if(!w) return; + free(w->tls_auth_name); if(w->timer) comm_timer_delete(w->timer); free(w); @@ -2540,7 +2541,16 @@ pending_tcp_query(struct serviced_query* sq, sldns_buffer* packet, w->cb = callback; w->cb_arg = callback_arg; w->ssl_upstream = sq->ssl_upstream; - w->tls_auth_name = sq->tls_auth_name; + if(sq->tls_auth_name) { + w->tls_auth_name = strdup(sq->tls_auth_name); + if(!w->tls_auth_name) { + comm_timer_delete(w->timer); + free(w); + return NULL; + } + } else { + w->tls_auth_name = NULL; + } w->timeout = timeout; w->id_node.key = NULL; w->write_wait_prev = NULL; diff --git a/services/outside_network.h b/services/outside_network.h index e30ce92eb..18f86ce05 100644 --- a/services/outside_network.h +++ b/services/outside_network.h @@ -419,7 +419,7 @@ struct waiting_tcp { void* cb_arg; /** if it uses ssl upstream */ int ssl_upstream; - /** ref to the tls_auth_name from the serviced_query */ + /** owned copy of the tls_auth_name (malloced) */ char* tls_auth_name; /** the packet was involved in an error, to stop looping errors */ int error_count; From 02b16de1ae40e43a3e3804e98ab9868da33d72eb Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:14:04 +0200 Subject: [PATCH 13/26] - Fix CVE-2026-50243, 'response-ip'/'rpz' can rewrite BOGUS answers instead of returning SERVFAIL. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- respip/respip.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/respip/respip.c b/respip/respip.c index ff12114de..0a7dedd07 100644 --- a/respip/respip.c +++ b/respip/respip.c @@ -1114,7 +1114,13 @@ respip_operate(struct module_qstate* qstate, enum module_ev event, int id, if((qstate->qinfo.qtype == LDNS_RR_TYPE_A || qstate->qinfo.qtype == LDNS_RR_TYPE_AAAA || qstate->qinfo.qtype == LDNS_RR_TYPE_ANY) && - qstate->return_msg && qstate->return_msg->rep) { + qstate->return_msg && qstate->return_msg->rep && + !(qstate->env->need_to_validate && + (!(qstate->query_flags & BIT_CD) + || qstate->env->cfg->ignore_cd) && + (qstate->return_msg->rep->security <= sec_status_bogus + || qstate->return_msg->rep->security == + sec_status_secure_sentinel_fail))) { struct reply_info* new_rep = qstate->return_msg->rep; struct ub_packed_rrset_key* alias_rrset = NULL; struct respip_action_info actinfo = {0, 0, 0, 0, NULL, 0, NULL}; From 3530c81e29e64ed19c612ae3dea21c8800d882e1 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:14:35 +0200 Subject: [PATCH 14/26] - Fix CVE-2026-50248, BOGUS configured primary hostname accepted for XFR in auth/rpz zones. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/authzone.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/services/authzone.c b/services/authzone.c index ebcdc7e43..e7a553916 100644 --- a/services/authzone.c +++ b/services/authzone.c @@ -5745,8 +5745,7 @@ xfr_master_add_addrs(struct auth_master* m, struct ub_packed_rrset_key* rrset, /** callback for task_transfer lookup of host name, of A or AAAA */ void auth_xfer_transfer_lookup_callback(void* arg, int rcode, sldns_buffer* buf, - enum sec_status ATTR_UNUSED(sec), char* ATTR_UNUSED(why_bogus), - int ATTR_UNUSED(was_ratelimited)) + enum sec_status sec, char* why_bogus, int ATTR_UNUSED(was_ratelimited)) { struct auth_xfer* xfr = (struct auth_xfer*)arg; struct module_env* env; @@ -5759,7 +5758,16 @@ void auth_xfer_transfer_lookup_callback(void* arg, int rcode, sldns_buffer* buf, } /* process result */ - if(rcode == LDNS_RCODE_NOERROR) { + if(sec == sec_status_bogus || sec == sec_status_secure_sentinel_fail) { + if(verbosity >= VERB_OPS) { + char zname[LDNS_MAX_DOMAINLEN]; + dname_str(xfr->name, zname); + verbose(VERB_OPS, "auth zone %s: primary %s address lookup is DNSSEC bogus: %s", + zname, xfr->task_transfer->lookup_target->host, + (why_bogus?why_bogus:"")); + } + /* fall through to next-lookup / next-master */ + } else if(rcode == LDNS_RCODE_NOERROR) { uint16_t wanted_qtype = LDNS_RR_TYPE_A; struct regional* temp = env->scratch; struct query_info rq; @@ -6830,8 +6838,7 @@ xfr_probe_send_or_end(struct auth_xfer* xfr, struct module_env* env) /** callback for task_probe lookup of host name, of A or AAAA */ void auth_xfer_probe_lookup_callback(void* arg, int rcode, sldns_buffer* buf, - enum sec_status ATTR_UNUSED(sec), char* ATTR_UNUSED(why_bogus), - int ATTR_UNUSED(was_ratelimited)) + enum sec_status sec, char* why_bogus, int ATTR_UNUSED(was_ratelimited)) { struct auth_xfer* xfr = (struct auth_xfer*)arg; struct module_env* env; @@ -6844,7 +6851,16 @@ void auth_xfer_probe_lookup_callback(void* arg, int rcode, sldns_buffer* buf, } /* process result */ - if(rcode == LDNS_RCODE_NOERROR) { + if(sec == sec_status_bogus || sec == sec_status_secure_sentinel_fail) { + if(verbosity >= VERB_OPS) { + char zname[LDNS_MAX_DOMAINLEN]; + dname_str(xfr->name, zname); + verbose(VERB_OPS, "auth zone %s: primary %s address probe lookup is DNSSEC bogus: %s", + zname, xfr->task_transfer->lookup_target->host, + (why_bogus?why_bogus:"")); + } + /* fall through to next-lookup / next-master */ + } else if(rcode == LDNS_RCODE_NOERROR) { uint16_t wanted_qtype = LDNS_RR_TYPE_A; struct regional* temp = env->scratch; struct query_info rq; From e180b06298d8d39a764d3c5d4d4aca472c3a97d7 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:15:02 +0200 Subject: [PATCH 15/26] - Fix CVE-2026-50251, Attacker supplied `0.0.0.0`/`::` glue triggers defensive full-cache flush. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- doc/unbound.conf.rst | 7 +++++++ iterator/iter_donotq.c | 12 ++++++++++++ testdata/dns_error_reporting.rpl | 1 + 3 files changed, 20 insertions(+) diff --git a/doc/unbound.conf.rst b/doc/unbound.conf.rst index cc01451cd..2e37e2e5d 100644 --- a/doc/unbound.conf.rst +++ b/doc/unbound.conf.rst @@ -2055,6 +2055,13 @@ These options are part of the ``server:`` section. flushing away any poison. A value of 10 million is suggested. + It is useful to add 0.0.0.0/8 and '::' to the + :ref:`do-not-query-address` list. + Otherwise they may be answered, from localhost, and the different source + makes an unwanted reply that unnecessarily ticks up. + The :ref:`do-not-query-localhost` + option includes them, the zero subnets, when it is enabled. + Default: 0 (disabled) diff --git a/iterator/iter_donotq.c b/iterator/iter_donotq.c index 40ffb45c4..7eecf1354 100644 --- a/iterator/iter_donotq.c +++ b/iterator/iter_donotq.c @@ -132,6 +132,18 @@ donotq_apply_cfg(struct iter_donotq* dq, struct config_file* cfg) if(cfg->do_ip6) { if(!donotq_str_cfg(dq, "::1")) return 0; + if(!donotq_str_cfg(dq, "::ffff:127.0.0.0/104")) + return 0; + } + /* RFC 1122 3.2.1.3 / RFC 6890 / RFC 4291 2.5.2: not valid as + * destination; on Linux these route to the local host. */ + if(!donotq_str_cfg(dq, "0.0.0.0/8")) + return 0; + if(cfg->do_ip6) { + if(!donotq_str_cfg(dq, "::")) + return 0; + if(!donotq_str_cfg(dq, "::ffff:0:0/96")) + return 0; } } addr_tree_init_parents(&dq->tree); diff --git a/testdata/dns_error_reporting.rpl b/testdata/dns_error_reporting.rpl index f1fac12a2..22175cade 100644 --- a/testdata/dns_error_reporting.rpl +++ b/testdata/dns_error_reporting.rpl @@ -12,6 +12,7 @@ server: ede: no # It is not needed for dns-error-reporting; only for clients to receive EDEs dns-error-reporting: yes do-ip6: no + do-not-query-localhost: no stub-zone: name: domain From 804cff4c152a121961b04605f75132370fc80df4 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:15:31 +0200 Subject: [PATCH 16/26] - Fix CVE-2026-50252, Possible cache poisoning attack by mapping source port population per thread. Thanks to Inbal Schussheim and Amit Klein, Hebrew University, for the report. --- daemon/daemon.c | 12 +- daemon/daemon.h | 3 + daemon/worker.c | 15 +- daemon/worker.h | 8 +- libunbound/libworker.c | 13 +- libunbound/libworker.h | 3 + services/outside_network.c | 376 ++++++++++++++++++++++++++++++------- services/outside_network.h | 105 ++++++++++- testcode/fake_event.c | 21 ++- testcode/unittcpreuse.c | 276 +++++++++++++++++++++++++++ 10 files changed, 728 insertions(+), 104 deletions(-) diff --git a/daemon/daemon.c b/daemon/daemon.c index ea4e83e70..51dd51de3 100644 --- a/daemon/daemon.c +++ b/daemon/daemon.c @@ -79,6 +79,7 @@ #include "util/tcp_conn_limit.h" #include "util/edns.h" #include "services/listen_dnsport.h" +#include "services/outside_network.h" #include "services/cache/rrset.h" #include "services/cache/infra.h" #include "services/localzone.h" @@ -813,6 +814,10 @@ daemon_create_workers(struct daemon* daemon) fatal_exit("out of memory during daemon init"); numport = daemon_get_shufport(daemon, shufport); verbose(VERB_ALGO, "total of %d outgoing ports available", numport); + if(!(daemon->shared_ports = shared_ports_create(daemon->cfg->out_ifs, + daemon->cfg->num_out_ifs, daemon->cfg->do_ip4, + daemon->cfg->do_ip6, shufport, numport))) + fatal_exit("could not setup shared ports: out of memory"); #ifdef HAVE_NGTCP2 if (cfg_has_quic(daemon->cfg)) { @@ -843,10 +848,7 @@ daemon_create_workers(struct daemon* daemon) #endif } for(i=0; inum; i++) { - if(!(daemon->workers[i] = worker_create(daemon, i, - shufport+numport*i/daemon->num, - numport*(i+1)/daemon->num - numport*i/daemon->num))) - /* the above is not ports/numthr, due to rounding */ + if(!(daemon->workers[i] = worker_create(daemon, i))) fatal_exit("could not create worker"); } /* create per-worker alloc caches if not reusing existing ones. */ @@ -1204,6 +1206,8 @@ daemon_cleanup(struct daemon* daemon) if(!daemon->reuse_cache || daemon->need_to_exit) daemon_clear_allocs(daemon); daemon->num = 0; + shared_ports_delete(daemon->shared_ports); + daemon->shared_ports = NULL; #ifdef USE_DNSTAP dt_delete(daemon->dtenv); daemon->dtenv = NULL; diff --git a/daemon/daemon.h b/daemon/daemon.h index 20386d7fc..e6f099629 100644 --- a/daemon/daemon.h +++ b/daemon/daemon.h @@ -62,6 +62,7 @@ struct doq_table; struct cookie_secrets; struct fast_reload_thread; struct fast_reload_printq; +struct shared_ports; #include "dnstap/dnstap_config.h" #ifdef USE_DNSTAP @@ -97,6 +98,8 @@ struct daemon { int rc_port; /** listening ports for remote control */ struct listen_port* rc_ports; + /** the shared ports structure, with random ports numbers. */ + struct shared_ports* shared_ports; /** remote control connections management (for first worker) */ struct daemon_remote* rc; /** ssl context for listening to dnstcp over ssl */ diff --git a/daemon/worker.c b/daemon/worker.c index 1d61f8733..765fb2299 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -2233,23 +2233,16 @@ void worker_probe_timer_cb(void* arg) } struct worker* -worker_create(struct daemon* daemon, int id, int* ports, int n) +worker_create(struct daemon* daemon, int id) { unsigned int seed; struct worker* worker = (struct worker*)calloc(1, sizeof(struct worker)); if(!worker) return NULL; - worker->numports = n; - worker->ports = (int*)memdup(ports, sizeof(int)*n); - if(!worker->ports) { - free(worker); - return NULL; - } worker->daemon = daemon; worker->thread_num = id; if(!(worker->cmd = tube_create())) { - free(worker->ports); free(worker); return NULL; } @@ -2257,7 +2250,6 @@ worker_create(struct daemon* daemon, int id, int* ports, int n) if(!(worker->rndstate = ub_initstate(daemon->rand))) { log_err("could not init random numbers."); tube_delete(worker->cmd); - free(worker->ports); free(worker); return NULL; } @@ -2356,14 +2348,14 @@ worker_init(struct worker* worker, struct config_file *cfg, cfg->out_ifs, cfg->num_out_ifs, cfg->do_ip4, cfg->do_ip6, cfg->do_tcp?cfg->outgoing_num_tcp:0, cfg->ip_dscp, worker->daemon->env->infra_cache, worker->rndstate, - cfg->use_caps_bits_for_id, worker->ports, worker->numports, + cfg->use_caps_bits_for_id, cfg->unwanted_threshold, cfg->outgoing_tcp_mss, &worker_alloc_cleanup, worker, cfg->do_udp || cfg->udp_upstream_without_downstream, worker->daemon->connect_dot_sslctx, cfg->delay_close, cfg->tls_use_sni, dtenv, cfg->udp_connect, cfg->max_reuse_tcp_queries, cfg->tcp_reuse_timeout, - cfg->tcp_auth_query_timeout); + cfg->tcp_auth_query_timeout, worker->daemon->shared_ports); if(!worker->back) { log_err("could not create outgoing sockets"); worker_delete(worker); @@ -2514,7 +2506,6 @@ worker_delete(struct worker* worker) tube_delete(worker->cmd); comm_timer_delete(worker->stat_timer); comm_timer_delete(worker->env.probe_timer); - free(worker->ports); if(worker->thread_num == 0) { #ifdef UB_ON_WINDOWS wsvc_desetup_worker(worker); diff --git a/daemon/worker.h b/daemon/worker.h index b7bb52fd7..37f3728ef 100644 --- a/daemon/worker.h +++ b/daemon/worker.h @@ -104,10 +104,6 @@ struct worker { struct listen_dnsport* front; /** the backside outside network interface to the auth servers */ struct outside_network* back; - /** ports to be used by this worker. */ - int* ports; - /** number of ports for this worker */ - int numports; /** the signal handler */ struct comm_signal* comsig; /** commpoint to listen to commands. */ @@ -146,11 +142,9 @@ struct worker { * with backpointers only. Use worker_init on it later. * @param daemon: the daemon that this worker thread is part of. * @param id: the thread number from 0.. numthreads-1. - * @param ports: the ports it is allowed to use, array. - * @param n: the number of ports. * @return: the new worker or NULL on alloc failure. */ -struct worker* worker_create(struct daemon* daemon, int id, int* ports, int n); +struct worker* worker_create(struct daemon* daemon, int id); /** * Initialize worker. diff --git a/libunbound/libworker.c b/libunbound/libworker.c index 6e7244c03..d70527f59 100644 --- a/libunbound/libworker.c +++ b/libunbound/libworker.c @@ -105,6 +105,7 @@ libworker_delete_env(struct libworker* w) SSL_CTX_free(w->sslctx); #endif outside_network_delete(w->back); + shared_ports_delete(w->shared_ports); } /** delete libworker struct */ @@ -219,17 +220,25 @@ libworker_setup(struct ub_ctx* ctx, int is_bg, struct ub_event_base* eb) libworker_delete(w); return NULL; } + if(!(w->shared_ports = shared_ports_create(cfg->out_ifs, + cfg->num_out_ifs, cfg->do_ip4, cfg->do_ip6, ports, numports))) { + if(!w->is_bg || w->is_bg_thread) { + lock_basic_unlock(&ctx->cfglock); + } + libworker_delete(w); + return NULL; + } w->back = outside_network_create(w->base, cfg->msg_buffer_size, (size_t)cfg->outgoing_num_ports, cfg->out_ifs, cfg->num_out_ifs, cfg->do_ip4, cfg->do_ip6, cfg->do_tcp?cfg->outgoing_num_tcp:0, cfg->ip_dscp, w->env->infra_cache, w->env->rnd, cfg->use_caps_bits_for_id, - ports, numports, cfg->unwanted_threshold, + cfg->unwanted_threshold, cfg->outgoing_tcp_mss, &libworker_alloc_cleanup, w, cfg->do_udp || cfg->udp_upstream_without_downstream, w->sslctx, cfg->delay_close, cfg->tls_use_sni, NULL, cfg->udp_connect, cfg->max_reuse_tcp_queries, cfg->tcp_reuse_timeout, - cfg->tcp_auth_query_timeout); + cfg->tcp_auth_query_timeout, w->shared_ports); w->env->outnet = w->back; if(!w->is_bg || w->is_bg_thread) { lock_basic_unlock(&ctx->cfglock); diff --git a/libunbound/libworker.h b/libunbound/libworker.h index 42aa5bae3..f527cb093 100644 --- a/libunbound/libworker.h +++ b/libunbound/libworker.h @@ -60,6 +60,7 @@ struct tube; struct sldns_buffer; struct ub_event_base; struct query_info; +struct shared_ports; /** * The library-worker status structure @@ -84,6 +85,8 @@ struct libworker { struct comm_base* base; /** the backside outside network interface to the auth servers */ struct outside_network* back; + /** shared ports structure */ + struct shared_ports* shared_ports; /** random() table for this worker. */ struct ub_randstate* rndstate; /** sslcontext for SSL wrapped DNS over TCP queries */ diff --git a/services/outside_network.c b/services/outside_network.c index 0fff18666..9dfa8b4d0 100644 --- a/services/outside_network.c +++ b/services/outside_network.c @@ -1481,7 +1481,7 @@ portcomm_loweruse(struct outside_network* outnet, struct port_comm* pc) pif = pc->pif; log_assert(pif->inuse > 0); #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - pif->avail_ports[pif->avail_total - pif->inuse] = pc->number; + shared_ports_return_port(outnet->shared_ports, pif->shpif, pc->number); #endif pif->inuse--; pif->out[pc->index] = pif->out[pif->inuse]; @@ -1695,19 +1695,19 @@ create_pending_tcp(struct outside_network* outnet, size_t bufsize) } /** setup an outgoing interface, ready address */ -static int setup_if(struct port_if* pif, const char* addrstr, - int* avail, int numavail, size_t numfd) +static int setup_if(struct port_if* pif, const char* addrstr, size_t numfd, + struct shared_ports* shp) { -#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - pif->avail_total = numavail; - pif->avail_ports = (int*)memdup(avail, (size_t)numavail*sizeof(int)); - if(!pif->avail_ports) - return 0; -#endif if(!ipstrtoaddr(addrstr, UNBOUND_DNS_PORT, &pif->addr, &pif->addrlen) && !netblockstrtoaddr(addrstr, UNBOUND_DNS_PORT, &pif->addr, &pif->addrlen, &pif->pfxlen)) return 0; +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + pif->shpif = shared_ports_find_if(shp, &pif->addr, pif->addrlen, + pif->pfxlen); +#else + (void)shp; +#endif pif->maxout = (int)numfd; pif->inuse = 0; pif->out = (struct port_comm**)calloc(numfd, @@ -1721,12 +1721,12 @@ struct outside_network* outside_network_create(struct comm_base *base, size_t bufsize, size_t num_ports, char** ifs, int num_ifs, int do_ip4, int do_ip6, size_t num_tcp, int dscp, struct infra_cache* infra, - struct ub_randstate* rnd, int use_caps_for_id, int* availports, - int numavailports, size_t unwanted_threshold, int tcp_mss, + struct ub_randstate* rnd, int use_caps_for_id, + size_t unwanted_threshold, int tcp_mss, void (*unwanted_action)(void*), void* unwanted_param, int do_udp, void* sslctx, int delayclose, int tls_use_sni, struct dt_env* dtenv, int udp_connect, int max_reuse_tcp_queries, int tcp_reuse_timeout, - int tcp_auth_query_timeout) + int tcp_auth_query_timeout, struct shared_ports* shared_ports) { struct outside_network* outnet = (struct outside_network*) calloc(1, sizeof(struct outside_network)); @@ -1761,6 +1761,7 @@ outside_network_create(struct comm_base *base, size_t bufsize, outnet->do_udp = do_udp; outnet->tcp_mss = tcp_mss; outnet->ip_dscp = dscp; + outnet->shared_ports = shared_ports; #ifndef S_SPLINT_S if(delayclose) { outnet->delayclose = 1; @@ -1771,7 +1772,7 @@ outside_network_create(struct comm_base *base, size_t bufsize, if(udp_connect) { outnet->udp_connect = 1; } - if(numavailports == 0 || num_ports == 0) { + if(num_ports == 0) { log_err("no outgoing ports available"); outside_network_delete(outnet); return NULL; @@ -1832,13 +1833,13 @@ outside_network_create(struct comm_base *base, size_t bufsize, /* allocate interfaces */ if(num_ifs == 0) { if(do_ip4 && !setup_if(&outnet->ip4_ifs[0], "0.0.0.0", - availports, numavailports, num_ports)) { + num_ports, outnet->shared_ports)) { log_err("malloc failed"); outside_network_delete(outnet); return NULL; } if(do_ip6 && !setup_if(&outnet->ip6_ifs[0], "::", - availports, numavailports, num_ports)) { + num_ports, outnet->shared_ports)) { log_err("malloc failed"); outside_network_delete(outnet); return NULL; @@ -1849,7 +1850,7 @@ outside_network_create(struct comm_base *base, size_t bufsize, for(i=0; iip6_ifs[done_6], ifs[i], - availports, numavailports, num_ports)){ + num_ports, outnet->shared_ports)){ log_err("malloc failed"); outside_network_delete(outnet); return NULL; @@ -1858,7 +1859,7 @@ outside_network_create(struct comm_base *base, size_t bufsize, } if(!str_is_ip6(ifs[i]) && do_ip4) { if(!setup_if(&outnet->ip4_ifs[done_4], ifs[i], - availports, numavailports, num_ports)){ + num_ports, outnet->shared_ports)){ log_err("malloc failed"); outside_network_delete(outnet); return NULL; @@ -1936,9 +1937,6 @@ outside_network_delete(struct outside_network* outnet) comm_point_delete(pc->cp); free(pc); } -#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - free(outnet->ip4_ifs[i].avail_ports); -#endif free(outnet->ip4_ifs[i].out); } free(outnet->ip4_ifs); @@ -1952,9 +1950,6 @@ outside_network_delete(struct outside_network* outnet) comm_point_delete(pc->cp); free(pc); } -#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - free(outnet->ip6_ifs[i].avail_ports); -#endif free(outnet->ip6_ifs[i].out); } free(outnet->ip6_ifs); @@ -2164,7 +2159,10 @@ static int select_ifport(struct outside_network* outnet, struct pending* pend, int num_if, struct port_if* ifs) { - int my_if, my_port, fd, portno, inuse, tries=0; + int my_if, fd, portno, inuse, tries=0; +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + int reused; +#endif struct port_if* pif; /* randomly select interface and port */ if(num_if == 0) { @@ -2178,37 +2176,35 @@ select_ifport(struct outside_network* outnet, struct pending* pend, my_if = ub_random_max(outnet->rnd, num_if); pif = &ifs[my_if]; #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - if(outnet->udp_connect) { - /* if we connect() we cannot reuse fds for a port */ - if(pif->inuse >= pif->avail_total) { - tries++; - if(tries < MAX_PORT_RETRY) - continue; - log_err("failed to find an open port, drop msg"); - return 0; - } - my_port = pif->inuse + ub_random_max(outnet->rnd, - pif->avail_total - pif->inuse); - } else { - my_port = ub_random_max(outnet->rnd, pif->avail_total); - if(my_port < pif->inuse) { - /* port already open */ - pend->pc = pif->out[my_port]; - verbose(VERB_ALGO, "using UDP if=%d port=%d", - my_if, pend->pc->number); - break; - } + if(!shared_ports_fetch_random(outnet->shared_ports, + pif->shpif, outnet->rnd, outnet->udp_connect, + pif->inuse, &portno, &reused)) { + tries++; + if(tries < MAX_PORT_RETRY) + continue; + log_err("failed to find an open port, drop msg"); + return 0; + } + if(reused) { + /* port already open */ + log_assert(portno < pif->inuse); + pend->pc = pif->out[portno]; + verbose(VERB_ALGO, "using UDP if=%d port=%d", + my_if, pend->pc->number); + break; } - /* try to open new port, if fails, loop to try again */ - log_assert(pif->inuse < pif->maxout); - portno = pif->avail_ports[my_port - pif->inuse]; #else - my_port = portno = 0; + portno = 0; #endif + /* try to open new port, if fails, loop to try again */ fd = udp_sockport(&pif->addr, pif->addrlen, pif->pfxlen, portno, &inuse, outnet->rnd, outnet->ip_dscp); if(fd == -1 && !inuse) { /* nonrecoverable error making socket */ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + shared_ports_return_port(outnet->shared_ports, + pif->shpif, portno); +#endif return 0; } if(fd != -1) { @@ -2225,6 +2221,11 @@ select_ifport(struct outside_network* outnet, struct pending* pend, pend->addrlen); } sock_close(fd); +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + shared_ports_return_port( + outnet->shared_ports, + pif->shpif, portno); +#endif return 0; } } @@ -2242,14 +2243,14 @@ select_ifport(struct outside_network* outnet, struct pending* pend, /* grab port in interface */ pif->out[pif->inuse] = pend->pc; -#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - pif->avail_ports[my_port - pif->inuse] = - pif->avail_ports[pif->avail_total-pif->inuse-1]; -#endif pif->inuse++; break; } /* failed, already in use */ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + shared_ports_return_port(outnet->shared_ports, pif->shpif, + portno); +#endif verbose(VERB_QUERY, "port %d in use, trying another", portno); tries++; if(tries == MAX_PORT_RETRY) { @@ -3640,13 +3641,16 @@ fd_for_dest(struct outside_network* outnet, struct sockaddr_storage* to_addr, { struct sockaddr_storage* addr; socklen_t addrlen; - int i, try, pnum, dscp; + int i, try, dscp; struct port_if* pif; /* create fd */ dscp = outnet->ip_dscp; for(try = 0; try<1000; try++) { int port = 0; +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + int reused = 0; +#endif int freebind = 0; int noproto = 0; int inuse = 0; @@ -3675,16 +3679,18 @@ fd_for_dest(struct outside_network* outnet, struct sockaddr_storage* to_addr, addr = &pif->addr; addrlen = pif->addrlen; #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - pnum = ub_random_max(outnet->rnd, pif->avail_total); - if(pnum < pif->inuse) { - /* port already open */ - port = pif->out[pnum]->number; - } else { - /* unused ports in start part of array */ - port = pif->avail_ports[pnum - pif->inuse]; + if(!shared_ports_fetch_random(outnet->shared_ports, + pif->shpif, outnet->rnd, 0, pif->inuse, + &port, &reused)) { + /* try again, perhaps another interface. */ + continue; + } + if(reused) { + log_assert(port < pif->inuse); + port = pif->out[port]->number; } #else - pnum = port = 0; + port = 0; #endif if(addr_is_ip6(to_addr, to_addrlen)) { struct sockaddr_in6 sa = *(struct sockaddr_in6*)addr; @@ -3699,6 +3705,14 @@ fd_for_dest(struct outside_network* outnet, struct sockaddr_storage* to_addr, (struct sockaddr*)addr, addrlen, 1, &inuse, &noproto, 0, 0, 0, NULL, 0, freebind, 0, dscp); } +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + if(!reused) { + /* Return the port to the pool, since the caller does + * not keep track of it, also have done fd, and bind. */ + shared_ports_return_port(outnet->shared_ports, + pif->shpif, port); + } +#endif if(fd != -1) { return fd; } @@ -3929,11 +3943,7 @@ if_get_mem(struct port_if* pif) { size_t s; int i; - s = sizeof(*pif) + -#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - sizeof(int)*pif->avail_total + -#endif - sizeof(struct port_comm*)*pif->maxout; + s = sizeof(*pif) + sizeof(struct port_comm*)*pif->maxout; for(i=0; iinuse; i++) s += sizeof(*pif->out[i]) + comm_point_get_mem(pif->out[i]->cp); @@ -4021,3 +4031,237 @@ serviced_get_mem(struct serviced_query* sq) return s; } +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION +/** Setup shared port interface */ +static int shared_ports_setup_if(struct shared_ports_if* shpif, char* str, + int* availports, int numavailports) +{ + shpif->avail_ports = (int*)memdup(availports, + (size_t)numavailports*sizeof(int)); + if(!shpif->avail_ports) + return 0; + shpif->avail_total = numavailports; + shpif->inuse = 0; + shpif->pfxlen = 0; + if(!ipstrtoaddr(str, UNBOUND_DNS_PORT, &shpif->addr, &shpif->addrlen) && + !netblockstrtoaddr(str, UNBOUND_DNS_PORT, &shpif->addr, + &shpif->addrlen, &shpif->pfxlen)) + return 0; + return 1; +} +#endif + +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION +/** Allocate shared ports interfaces */ +static int shared_ports_alloc_ifs(struct shared_ports* shp, char** ifs, + int num_ifs, int do_ip4, int do_ip6, int* availports, + int numavailports) +{ +#ifndef INET6 + do_ip6 = 0; +#endif + calc_num46(ifs, num_ifs, do_ip4, do_ip6, + &shp->num_ip4, &shp->num_ip6); + if(shp->num_ip4 != 0) { + if(!(shp->ip4_ifs = (struct shared_ports_if*)calloc( + (size_t)shp->num_ip4, + sizeof(struct shared_ports_if)))) + return 0; + } + if(shp->num_ip6 != 0) { + if(!(shp->ip6_ifs = (struct shared_ports_if*)calloc( + (size_t)shp->num_ip6, + sizeof(struct shared_ports_if)))) + return 0; + } + if(num_ifs == 0) { + if(do_ip4 && !shared_ports_setup_if(&shp->ip4_ifs[0], + "0.0.0.0", availports, numavailports)) + return 0; + if(do_ip6 && !shared_ports_setup_if(&shp->ip6_ifs[0], + "::", availports, numavailports)) + return 0; + } else { + size_t done_4 = 0, done_6 = 0; + int i; + for(i=0; iip6_ifs[done_6], + ifs[i], availports, numavailports)) + return 0; + done_6++; + } + if(!str_is_ip6(ifs[i]) && do_ip4) { + if(!shared_ports_setup_if(&shp->ip4_ifs[done_4], + ifs[i], availports, numavailports)) + return 0; + done_4++; + } + } + } + return 1; +} +#endif + +struct shared_ports* shared_ports_create(char** ifs, int num_ifs, int do_ip4, + int do_ip6, int* availports, int numavailports) +{ + struct shared_ports* shp = calloc(1, sizeof(*shp)); + if(!shp) { + log_err("malloc failed"); + return NULL; + } + lock_basic_init(&shp->lock); + lock_protect(&shp->lock, shp, sizeof(*shp)); + +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + /* Allocate interfaces */ + if(!shared_ports_alloc_ifs(shp, ifs, num_ifs, do_ip4, do_ip6, + availports, numavailports)) { + log_err("malloc failed"); + shared_ports_delete(shp); + return NULL; + } +#else + (void)ifs; (void)num_ifs; (void)do_ip4; (void)do_ip6; + (void)availports; (void)numavailports; +#endif + return shp; +} + +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION +/** Delete shared ports interface structure */ +static void shared_ports_if_delete(struct shared_ports_if* shpif) +{ + if(!shpif) + return; + free(shpif->avail_ports); +} +#endif + +void shared_ports_delete(struct shared_ports* shp) +{ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + int i; +#endif + if(!shp) + return; + lock_basic_destroy(&shp->lock); +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + for(i=0; inum_ip4; i++) { + shared_ports_if_delete(&shp->ip4_ifs[i]); + } + free(shp->ip4_ifs); + for(i=0; inum_ip6; i++) { + shared_ports_if_delete(&shp->ip6_ifs[i]); + } + free(shp->ip6_ifs); +#endif + free(shp); +} + +struct shared_ports_if* shared_ports_find_if(struct shared_ports* shp, + struct sockaddr_storage* addr, socklen_t addrlen, int pfxlen) +{ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + struct shared_ports_if* ret, *ifs = NULL; + int i, num_ifs = 0; + lock_basic_lock(&shp->lock); + if(addr_is_ip6(addr, addrlen)) { + ifs = shp->ip6_ifs; + num_ifs = shp->num_ip6; + } else { + ifs = shp->ip4_ifs; + num_ifs = shp->num_ip4; + } + for(i=0; ilock); + return ret; + } + } + lock_basic_unlock(&shp->lock); + return NULL; +#else + (void)shp; (void)addr; (void)addrlen; (void)pfxlen; + return NULL; +#endif +} + +int shared_ports_fetch_random(struct shared_ports* shp, + struct shared_ports_if* shpif, struct ub_randstate* rnd, + int udp_connect, int reusenum, int* port, int* reused) +{ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + int portno = 0, my_port = 0; + if(!shpif) + return 0; + lock_basic_lock(&shp->lock); + if(udp_connect) { + /* if we connect() we cannot reuse fds for a port. */ + if(shpif->inuse >= shpif->avail_total) { + lock_basic_unlock(&shp->lock); + return 0; + } + my_port = ub_random_max(rnd, + shpif->avail_total - shpif->inuse); + } else { + /* select from free ports and open ports on this thread. */ + if(shpif->inuse >= shpif->avail_total) { + lock_basic_unlock(&shp->lock); + if(reusenum == 0) { + return 0; + } + my_port = ub_random_max(rnd, reusenum); + *port = my_port; + *reused = 1; + return 1; + } + my_port = ub_random_max(rnd, shpif->avail_total - shpif->inuse + + reusenum); + if(my_port < reusenum) { + /* port already open */ + lock_basic_unlock(&shp->lock); + *port = my_port; + *reused = 1; + return 1; + } + my_port -= reusenum; + } + log_assert(shpif->inuse < shpif->avail_total); + log_assert(my_port >= 0 && my_port < shpif->avail_total); + portno = shpif->avail_ports[my_port]; + shpif->avail_ports[my_port] = + shpif->avail_ports[shpif->avail_total-shpif->inuse-1]; + shpif->inuse++; + lock_basic_unlock(&shp->lock); + *port = portno; + *reused = 0; + return 1; +#else + (void)shp; (void)shpif; (void)rnd; (void)udp_connect; + (void)reusenum; + *port = 0; + *reused = 0; + return 1; +#endif +} + +void shared_ports_return_port(struct shared_ports* shp, + struct shared_ports_if* shpif, int port) +{ +#ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION + if(!shpif) + return; + lock_basic_lock(&shp->lock); + log_assert(shpif->inuse > 0); + shpif->avail_ports[shpif->avail_total - shpif->inuse] = port; + shpif->inuse--; + lock_basic_unlock(&shp->lock); +#else + (void)shp; (void)shpif; (void)port; +#endif +} diff --git a/services/outside_network.h b/services/outside_network.h index 18f86ce05..8841c607d 100644 --- a/services/outside_network.h +++ b/services/outside_network.h @@ -70,6 +70,8 @@ struct module_env; struct module_qstate; struct query_info; struct config_file; +struct shared_ports; +struct shared_ports_if; /** * Send queries to outside servers and wait for answers from servers. @@ -119,6 +121,9 @@ struct outside_network { int udp_connect; /** number of udp packets sent. */ size_t num_udp_outgoing; + /** the shared ports structure, with random ports numbers. + * This is a reference to the member in the daemon structure. */ + struct shared_ports* shared_ports; /** array of outgoing IP4 interfaces */ struct port_if* ip4_ifs; @@ -211,11 +216,8 @@ struct port_if { int pfxlen; #ifndef DISABLE_EXPLICIT_PORT_RANDOMISATION - /** the available ports array. These are unused. - * Only the first total-inuse part is filled. */ - int* avail_ports; - /** the total number of available ports (size of the array) */ - int avail_total; + /** the shared port numbers for this interface. */ + struct shared_ports_if* shpif; #endif /** array of the commpoints currently in use. @@ -245,6 +247,42 @@ struct port_comm { struct comm_point* cp; }; +/** + * Shared ports, the list of ports shared across threads + */ +struct shared_ports { + /** mutex on the ports */ + lock_basic_type lock; + /** array of IP4 interfaces */ + struct shared_ports_if* ip4_ifs; + /** number of outgoing IP4 interfaces */ + int num_ip4; + /** array of IP6 interfaces */ + struct shared_ports_if* ip6_ifs; + /** number of outgoing IP6 interfaces */ + int num_ip6; +}; + +/** + * Shared ports for an interface. + */ +struct shared_ports_if { + /** address ready to allocate new socket (except port no). */ + struct sockaddr_storage addr; + /** length of addr field */ + socklen_t addrlen; + /** if a netblock, the prefix */ + int pfxlen; + + /** the available ports array. These are unused. + * Only the first total-inuse part is filled. */ + int* avail_ports; + /** the total number of available ports (size of the array) */ + int avail_total; + /** the number in use. */ + int inuse; +}; + /** * Reuse TCP connection, still open can be used again. */ @@ -551,8 +589,6 @@ struct serviced_query { * @param infra: pointer to infra cached used for serviced queries. * @param rnd: stored to create random numbers for serviced queries. * @param use_caps_for_id: enable to use 0x20 bits to encode id randomness. - * @param availports: array of available ports. - * @param numavailports: number of available ports in array. * @param unwanted_threshold: when to take defensive action. * @param unwanted_action: the action to take. * @param unwanted_param: user parameter to action. @@ -567,17 +603,18 @@ struct serviced_query { * @param max_reuse_tcp_queries: max number of queries on a reuse connection. * @param tcp_reuse_timeout: timeout for REUSE entries in milliseconds. * @param tcp_auth_query_timeout: timeout in milliseconds for TCP queries to auth servers. + * @param shared_ports: the shared_ports structure. * @return: the new structure (with no pending answers) or NULL on error. */ struct outside_network* outside_network_create(struct comm_base* base, size_t bufsize, size_t num_ports, char** ifs, int num_ifs, int do_ip4, int do_ip6, size_t num_tcp, int dscp, struct infra_cache* infra, - struct ub_randstate* rnd, int use_caps_for_id, int* availports, - int numavailports, size_t unwanted_threshold, int tcp_mss, + struct ub_randstate* rnd, int use_caps_for_id, + size_t unwanted_threshold, int tcp_mss, void (*unwanted_action)(void*), void* unwanted_param, int do_udp, void* sslctx, int delayclose, int tls_use_sni, struct dt_env *dtenv, int udp_connect, int max_reuse_tcp_queries, int tcp_reuse_timeout, - int tcp_auth_query_timeout); + int tcp_auth_query_timeout, struct shared_ports* shared_ports); /** * Delete outside_network structure. @@ -819,6 +856,54 @@ struct comm_point* outnet_comm_point_for_http(struct outside_network* outnet, /** connect tcp connection to addr, 0 on failure */ int outnet_tcp_connect(int s, struct sockaddr_storage* addr, socklen_t addrlen); +/** + * Create new shared ports structure. + * @param ifs: interface names (or NULL for default interface). + * These interfaces must be able to access all authoritative servers. + * @param num_ifs: number of names in array ifs. + * @param do_ip4: service IP4. + * @param do_ip6: service IP6. + * @param availports: array of available ports. + * @param numavailports: number of available ports in array. + * @return new, or NULL on failure. + */ +struct shared_ports* shared_ports_create(char** ifs, int num_ifs, int do_ip4, + int do_ip6, int* availports, int numavailports); + +/** + * Delete shared ports structure. + * @param shp: shared ports structure. + */ +void shared_ports_delete(struct shared_ports* shp); + +/** Find interface in shared ports. */ +struct shared_ports_if* shared_ports_find_if(struct shared_ports* shp, + struct sockaddr_storage* addr, socklen_t addrlen, int pfxlen); + +/** + * Get a shared port from the list of random ports. + * @param shp: shared ports structure. + * @param shpif: the shared ports interface. + * @param rnd: used to make random numbers. + * @param udp_connect: set to true if no reuse is possible. + * @param reusenum: number of ports that can be reused (already open). + * @param port: the port number is returned. + * @param reused: if the port numer is reused, returned. + * @return false on failure. That can mean no more free ports to use. + */ +int shared_ports_fetch_random(struct shared_ports* shp, + struct shared_ports_if* shpif, struct ub_randstate* rnd, + int udp_connect, int reusenum, int* port, int* reused); + +/** + * Return a shared port to the list of random ports. + * @param shp: shared ports structure. + * @param shpif: the shared ports interface. + * @param port: port number to return to be used again. + */ +void shared_ports_return_port(struct shared_ports* shp, + struct shared_ports_if* shpif, int port); + /** callback for incoming udp answers from the network */ int outnet_udp_cb(struct comm_point* c, void* arg, int error, struct comm_reply *reply_info); diff --git a/testcode/fake_event.c b/testcode/fake_event.c index b127d0df8..4ca357770 100644 --- a/testcode/fake_event.c +++ b/testcode/fake_event.c @@ -1126,15 +1126,16 @@ outside_network_create(struct comm_base* base, size_t bufsize, int ATTR_UNUSED(dscp), struct infra_cache* infra, struct ub_randstate* ATTR_UNUSED(rnd), - int ATTR_UNUSED(use_caps_for_id), int* ATTR_UNUSED(availports), - int ATTR_UNUSED(numavailports), size_t ATTR_UNUSED(unwanted_threshold), + int ATTR_UNUSED(use_caps_for_id), + size_t ATTR_UNUSED(unwanted_threshold), int ATTR_UNUSED(outgoing_tcp_mss), void (*unwanted_action)(void*), void* ATTR_UNUSED(unwanted_param), int ATTR_UNUSED(do_udp), void* ATTR_UNUSED(sslctx), int ATTR_UNUSED(delayclose), int ATTR_UNUSED(tls_use_sni), struct dt_env* ATTR_UNUSED(dtenv), int ATTR_UNUSED(udp_connect), int ATTR_UNUSED(max_reuse_tcp_queries), int ATTR_UNUSED(tcp_reuse_timeout), - int ATTR_UNUSED(tcp_auth_query_timeout)) + int ATTR_UNUSED(tcp_auth_query_timeout), + struct shared_ports* ATTR_UNUSED(shared_ports)) { struct replay_runtime* runtime = (struct replay_runtime*)base; struct outside_network* outnet = calloc(1, @@ -1980,6 +1981,20 @@ int outnet_tcp_connect(int ATTR_UNUSED(s), struct sockaddr_storage* ATTR_UNUSED( return 0; } +struct shared_ports* shared_ports_create(char** ATTR_UNUSED(ifs), + int ATTR_UNUSED(num_ifs), int ATTR_UNUSED(do_ip4), + int ATTR_UNUSED(do_ip6), int* ATTR_UNUSED(availports), + int ATTR_UNUSED(numavailports)) +{ + return calloc(1, sizeof(struct shared_ports)); +} + +void shared_ports_delete(struct shared_ports* shp) +{ + if(!shp) return; + free(shp); +} + int tcp_req_info_add_meshstate(struct tcp_req_info* ATTR_UNUSED(req), struct mesh_area* ATTR_UNUSED(mesh), struct mesh_state* ATTR_UNUSED(m)) { diff --git a/testcode/unittcpreuse.c b/testcode/unittcpreuse.c index 5f45a4b45..ce62e3325 100644 --- a/testcode/unittcpreuse.c +++ b/testcode/unittcpreuse.c @@ -41,6 +41,7 @@ #include "config.h" #include "testcode/unitmain.h" #include "util/log.h" +#include "util/net_help.h" #include "util/random.h" #include "services/outside_network.h" @@ -479,6 +480,278 @@ static void reuse_write_wait_test(void) check_reuse_write_wait_removal(1, &reuse, store, 0, 1); } +static void shared_port_test_ifs(void) +{ + struct shared_ports* shp; + struct shared_ports_if* shpif; + char* ifs[] = {"1.2.3.4", "1.2.3.5", "::1:2", "::1:3"}; + int availports[] = {1, 2, 3, 4}; + struct sockaddr_storage addr; + socklen_t addrlen; + + shp = shared_ports_create(ifs, 4, 1, 1, availports, 4); + unit_assert(shp); + + if(!ipstrtoaddr("1.2.3.4", UNBOUND_DNS_PORT, &addr, &addrlen)) + log_err("could not parse"); + shpif = shared_ports_find_if(shp, &addr, addrlen, 0); + unit_assert(shpif); + + if(!ipstrtoaddr("1.2.3.5", UNBOUND_DNS_PORT, &addr, &addrlen)) + log_err("could not parse"); + shpif = shared_ports_find_if(shp, &addr, addrlen, 0); + unit_assert(shpif); + + if(!ipstrtoaddr("::1:2", UNBOUND_DNS_PORT, &addr, &addrlen)) + log_err("could not parse"); + shpif = shared_ports_find_if(shp, &addr, addrlen, 0); + unit_assert(shpif); + + if(!ipstrtoaddr("::1:3", UNBOUND_DNS_PORT, &addr, &addrlen)) + log_err("could not parse"); + shpif = shared_ports_find_if(shp, &addr, addrlen, 0); + unit_assert(shpif); + + shared_ports_delete(shp); +} + +/** See if a port is on the shared_ports ports list */ +static int +pif_list_contains(struct shared_ports_if* shpif, int item) +{ + int i; + unit_assert(shpif->inuse >= 0 && shpif->inuse <= shpif->avail_total); + for(i=0; i< shpif->avail_total - shpif->inuse; i++) { + if(shpif->avail_ports[i] == item) + return 1; + } + return 0; +} + +/** See if a number of ports are on the shared_ports list */ +static int +pif_list_contains_items(struct shared_ports_if* shpif, int item1, + int item2, int item3, int item4) +{ + if(item1 != -1 && !pif_list_contains(shpif, item1)) + return 0; + if(item2 != -1 && !pif_list_contains(shpif, item2)) + return 0; + if(item3 != -1 && !pif_list_contains(shpif, item3)) + return 0; + if(item4 != -1 && !pif_list_contains(shpif, item4)) + return 0; + return 1; +} + +static void shared_port_test_port(void) +{ + struct shared_ports* shp; + struct shared_ports_if* shpif; + char* ifs[] = {"1.2.3.4", "1.2.3.5"}; + int availports[] = {1, 2, 3, 4}; + struct sockaddr_storage addr; + socklen_t addrlen; + int p1, p2, p3, reused; + struct ub_randstate* rnd; + + rnd = ub_initstate(NULL); + unit_assert(rnd); + + shp = shared_ports_create(ifs, 2, 1, 1, availports, 4); + unit_assert(shp); + + if(!ipstrtoaddr("1.2.3.4", UNBOUND_DNS_PORT, &addr, &addrlen)) + log_err("could not parse"); + shpif = shared_ports_find_if(shp, &addr, addrlen, 0); + unit_assert(shpif); + + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 0); + unit_assert(pif_list_contains_items(shpif, 1, 2, 3, 4)); + + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + unit_assert(!pif_list_contains(shpif, p1)); + if(p1 != 1) unit_assert(pif_list_contains(shpif, 1)); + if(p1 != 2) unit_assert(pif_list_contains(shpif, 2)); + if(p1 != 3) unit_assert(pif_list_contains(shpif, 3)); + if(p1 != 4) unit_assert(pif_list_contains(shpif, 4)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 1); + + shared_ports_return_port(shp, shpif, p1); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 0); + unit_assert(pif_list_contains_items(shpif, 1, 2, 3, 4)); + + /* pick up two items */ + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p2, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p2 != 0); + unit_assert(!pif_list_contains(shpif, p1)); + unit_assert(!pif_list_contains(shpif, p2)); + if(p1 != 1 && p2 != 1) unit_assert(pif_list_contains(shpif, 1)); + if(p1 != 2 && p2 != 2) unit_assert(pif_list_contains(shpif, 2)); + if(p1 != 3 && p2 != 3) unit_assert(pif_list_contains(shpif, 3)); + if(p1 != 4 && p2 != 4) unit_assert(pif_list_contains(shpif, 4)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 2); + + shared_ports_return_port(shp, shpif, p1); + unit_assert(pif_list_contains(shpif, p1)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 1); + + shared_ports_return_port(shp, shpif, p2); + unit_assert(pif_list_contains(shpif, p2)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 0); + unit_assert(pif_list_contains_items(shpif, 1, 2, 3, 4)); + + /* pick up three items */ + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p2, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p2 != 0); + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p3, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p3 != 0); + unit_assert(!pif_list_contains(shpif, p1)); + unit_assert(!pif_list_contains(shpif, p2)); + unit_assert(!pif_list_contains(shpif, p3)); + if(p1 != 1 && p2 != 1 && p3 != 1) + unit_assert(pif_list_contains(shpif, 1)); + if(p1 != 2 && p2 != 2 && p3 != 2) + unit_assert(pif_list_contains(shpif, 2)); + if(p1 != 3 && p2 != 3 && p3 != 3) + unit_assert(pif_list_contains(shpif, 3)); + if(p1 != 4 && p2 != 4 && p3 != 4) + unit_assert(pif_list_contains(shpif, 4)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 3); + + shared_ports_return_port(shp, shpif, p1); + unit_assert(pif_list_contains(shpif, p1)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 2); + + shared_ports_return_port(shp, shpif, p2); + unit_assert(pif_list_contains(shpif, p2)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 1); + + shared_ports_return_port(shp, shpif, p3); + unit_assert(pif_list_contains(shpif, p3)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 0); + unit_assert(pif_list_contains_items(shpif, 1, 2, 3, 4)); + + /* pick up all four items */ + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0, 0, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 0); + unit_assert(p1 != 0); + unit_assert(!pif_list_contains(shpif, 1)); + unit_assert(!pif_list_contains(shpif, 2)); + unit_assert(!pif_list_contains(shpif, 3)); + unit_assert(!pif_list_contains(shpif, 4)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 4); + + /* more fetches fail, it is fully inuse. */ + unit_assert(!shared_ports_fetch_random(shp, shpif, rnd, 0, 0, &p2, + &reused)); + unit_assert(!shared_ports_fetch_random(shp, shpif, rnd, 0, 0, &p3, + &reused)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 4); + + /* reuse is then always the case */ + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0 /* can reuse */, 4 /* reusenum */, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 1); + unit_assert(p1 >= 0 && p1 < 4 /* reusenum */); + + if(!shared_ports_fetch_random(shp, shpif, rnd, + 0 /* can reuse */, 4 /* reusenum */, &p1, &reused)) { + unit_assert(0); /* should succeed */ + } + unit_assert(reused == 1); + unit_assert(p1 >= 0 && p1 < 4 /* reusenum */); + + /* return all the ports */ + shared_ports_return_port(shp, shpif, 1); + unit_assert(pif_list_contains(shpif, 1)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 3); + shared_ports_return_port(shp, shpif, 2); + unit_assert(pif_list_contains(shpif, 2)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 2); + shared_ports_return_port(shp, shpif, 3); + unit_assert(pif_list_contains(shpif, 3)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 1); + shared_ports_return_port(shp, shpif, 4); + unit_assert(pif_list_contains(shpif, 4)); + unit_assert(shpif->avail_total == 4); + unit_assert(shpif->inuse == 0); + unit_assert(pif_list_contains_items(shpif, 1, 2, 3, 4)); + + shared_ports_delete(shp); + ub_randfree(rnd); +} + void tcpreuse_test(void) { unit_show_feature("tcp_reuse"); @@ -486,4 +759,7 @@ void tcpreuse_test(void) tcp_reuse_tree_list_test(); waiting_tcp_list_test(); reuse_write_wait_test(); + unit_show_feature("shared_ports"); + shared_port_test_ifs(); + shared_port_test_port(); } From 8c702de175cb687d9645603ad3e8dc7c08a925e8 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:16:03 +0200 Subject: [PATCH 17/26] - Fix CVE-2026-52863, Memory corruption could lead to crash and denial of service. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/mesh.c | 8 +++++-- services/mesh.h | 4 ++++ testcode/unitmain.c | 56 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/services/mesh.c b/services/mesh.c index f06c1cb9d..cd78d1c29 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -933,8 +933,7 @@ cfg_region_strlist_copy(struct regional* region, struct config_strlist* list) return result; } -/** Copy the client info to the query region. */ -static struct respip_client_info* +struct respip_client_info* mesh_copy_client_info(struct regional* region, struct respip_client_info* cinfo) { size_t i; @@ -979,6 +978,11 @@ mesh_copy_client_info(struct regional* region, struct respip_client_info* cinfo) cinfo->view->name); if(!client_info->view_name) return NULL; + } else if(cinfo->view_name) { + client_info->view_name = regional_strdup(region, + cinfo->view_name); + if(!client_info->view_name) + return NULL; } return client_info; } diff --git a/services/mesh.h b/services/mesh.h index 352260e26..6ea63d098 100644 --- a/services/mesh.h +++ b/services/mesh.h @@ -738,4 +738,8 @@ void mesh_respond_serve_expired(struct mesh_state* mstate); void mesh_remove_callback(struct mesh_area* mesh, struct query_info* qinfo, uint16_t qflags, mesh_cb_func_type cb, void* cb_arg); +/** Copy the client info to the query region. */ +struct respip_client_info* mesh_copy_client_info(struct regional* region, + struct respip_client_info* cinfo); + #endif /* SERVICES_MESH_H */ diff --git a/testcode/unitmain.c b/testcode/unitmain.c index 4bc756a07..62f37375f 100644 --- a/testcode/unitmain.c +++ b/testcode/unitmain.c @@ -1282,6 +1282,61 @@ static void localzone_test(void) localzone_parents_test(); } +#include "services/mesh.h" +/** mesh unit tests */ +static void mesh_test(void) +{ + struct regional* r2, *r3; + struct respip_client_info* c1, *c2, *c3; + unit_show_func("services/mesh.c", "mesh_copy_client_info"); + r2 = regional_create(); + r3 = regional_create(); + if(!r2 || !r3) fatal_exit("out of memory"); + + c1 = calloc(1, sizeof(*c1)); + if(!c1) fatal_exit("out of memory"); + c1->view = calloc(1, sizeof(*c1->view)); + if(!c1->view) fatal_exit("out of memory"); + c1->view->name = strdup("view1"); + if(!c1->view->name) fatal_exit("out of memory"); + + c2 = mesh_copy_client_info(r2, c1); + if(!c2) fatal_exit("out of memory"); + c3 = mesh_copy_client_info(r3, c2); + if(!c3) fatal_exit("out of memory"); + + unit_assert(strcmp(c1->view->name, c2->view_name) == 0); + unit_assert(strcmp(c1->view->name, c3->view_name) == 0); + + /* make sure that the c3 view_name is in the r3 region. */ + unit_assert(r3->next == NULL); /* only the first chunk present atm */ + if(strlen(c3->view_name) >= r3->large_object_size) { + char* a = r3->large_list; + int found = 0; + while(a) { + if(strcmp(c3->view_name, + a + /* ALIGNEMENT */ sizeof(uint64_t)) == 0) { + found = 1; + break; + } + a = *(char**)a; + } + unit_assert(found == 1); + } else { + /* The allocation is expected in the r3 region first chunk */ + unit_assert((uint8_t*)c3->view_name < ((uint8_t*)r3)+r3->first_size); + } + + regional_destroy(r2); + /* ASAN should complain for the freed access below */ + unit_assert(strcmp(c1->view->name, c3->view_name) == 0); + + regional_destroy(r3); + free(c1->view->name); + free(c1->view); + free(c1); +} + void unit_show_func(const char* file, const char* func) { printf("test %s:%s\n", file, func); @@ -1356,6 +1411,7 @@ main(int argc, char* argv[]) msgparse_test(); edns_ede_answer_encode_test(); localzone_test(); + mesh_test(); #ifdef CLIENT_SUBNET ecs_test(); #endif /* CLIENT_SUBNET */ From 8a15ffee620bce05fbfd2c69b0d4c31c10a02431 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:16:42 +0200 Subject: [PATCH 18/26] - Fix CVE-2026-54478, DNS Cookie bypass when combined with proxy-protocol use. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- util/data/msgparse.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/util/data/msgparse.c b/util/data/msgparse.c index 9239f8fe3..7b2de102e 100644 --- a/util/data/msgparse.c +++ b/util/data/msgparse.c @@ -1068,13 +1068,13 @@ parse_edns_options_from_query(uint8_t* rdata_ptr, size_t rdata_len, * purposes. It will be overwritten if (re)creation * is needed. */ - if(repinfo->remote_addr.ss_family == AF_INET) { + if(repinfo->client_addr.ss_family == AF_INET) { memcpy(server_cookie + 16, - &((struct sockaddr_in*)&repinfo->remote_addr)->sin_addr, 4); + &((struct sockaddr_in*)&repinfo->client_addr)->sin_addr, 4); } else { cookie_is_v4 = 0; memcpy(server_cookie + 16, - &((struct sockaddr_in6*)&repinfo->remote_addr)->sin6_addr, 16); + &((struct sockaddr_in6*)&repinfo->client_addr)->sin6_addr, 16); } if(cfg->cookie_secret_file && From c29ff70f6aa9bb2f02e5f21001832f2b4791bd76 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:17:10 +0200 Subject: [PATCH 19/26] - Fix CVE-2026-55708, Privacy/configuration issue when adding local data in views through 'unbound-control'. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- daemon/remote.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/daemon/remote.c b/daemon/remote.c index 1eaf90016..61beb7c2f 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -1658,6 +1658,14 @@ do_view_data_add(RES* ssl, struct worker* worker, char* arg) ssl_printf(ssl,"error out of memory\n"); return; } + if(!v->isfirst) { + /* Global local-zone is not used for this view, + * therefore add defaults to this view-specific + * local-zone. */ + struct config_file lz_cfg; + memset(&lz_cfg, 0, sizeof(lz_cfg)); + local_zone_enter_defaults(v->local_zones, &lz_cfg); + } } do_data_add(ssl, v->local_zones, arg2); lock_rw_unlock(&v->lock); @@ -1683,6 +1691,14 @@ do_view_datas_add(struct daemon_remote* rc, RES* ssl, struct worker* worker, ssl_printf(ssl,"error out of memory\n"); return; } + if(!v->isfirst) { + /* Global local-zone is not used for this view, + * therefore add defaults to this view-specific + * local-zone. */ + struct config_file lz_cfg; + memset(&lz_cfg, 0, sizeof(lz_cfg)); + local_zone_enter_defaults(v->local_zones, &lz_cfg); + } } /* put the view name in the command buf */ (void)snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf), "%s ", arg); From 2ce2ca36912644d2dbf97249a41494a9e2500fc3 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:17:32 +0200 Subject: [PATCH 20/26] - Fix CVE-2026-55717, 'serve-expired-client-timeout' and 'response-ip' CNAME redirect could lead to a crash. Thanks to Qifan Zhang, Palo Alto Networks, for the report. In addition, thanks to Xin Wang, Jiapeng Li, and Jiajia Liu, Northwestern Polytechnical University, for also reporting this issue. --- services/localzone.h | 2 +- services/mesh.c | 15 ++++++++++----- util/data/packed_rrset.c | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/services/localzone.h b/services/localzone.h index 76c011836..e3fb0afe3 100644 --- a/services/localzone.h +++ b/services/localzone.h @@ -573,7 +573,7 @@ enum respip_action { respip_always_nxdomain = local_zone_always_nxdomain, /** answer with nodata response */ respip_always_nodata = local_zone_always_nodata, - /** answer with nodata response */ + /** drop query */ respip_always_deny = local_zone_always_deny, /** RPZ: truncate answer in order to force switch to tcp */ respip_truncate = local_zone_truncate, diff --git a/services/mesh.c b/services/mesh.c index cd78d1c29..8ce3dcee7 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -2454,9 +2454,10 @@ apply_respip_action(struct module_qstate* qstate, /* xxx_deny actions mean dropping the reply, unless the original reply * was redirected to response-ip data. */ - if((actinfo->action == respip_deny || + if(actinfo->action == respip_always_deny || + ((actinfo->action == respip_deny || actinfo->action == respip_inform_deny) && - *encode_repp == rep) + *encode_repp == rep)) *encode_repp = NULL; return 1; @@ -2521,12 +2522,15 @@ mesh_serve_expired_callback(void* arg) qstate->client_info, &actinfo, msg->rep, &alias_rrset, &encode_rep, qstate->env->auth_zones)) { return; - } else if(partial_rep && - !respip_merge_cname(partial_rep, &qstate->qinfo, msg->rep, + } else if(partial_rep) { + if(!respip_merge_cname(partial_rep, &qstate->qinfo, msg->rep, qstate->client_info, must_validate, &encode_rep, qstate->region, qstate->env->auth_zones, qstate->env->views, qstate->env->respip_set)) { - return; + return; + } + /* merge succeeded; final reply, no further alias pass */ + partial_rep = NULL; } if(!encode_rep || alias_rrset) { if(!encode_rep) { @@ -2537,6 +2541,7 @@ mesh_serve_expired_callback(void* arg) partial_rep = encode_rep; } } + msg->rep = encode_rep; /* We've found a partial reply ending with an * alias. Replace the lookup qinfo for the * alias target and lookup the cache again to diff --git a/util/data/packed_rrset.c b/util/data/packed_rrset.c index 89ece3c03..b695c1b71 100644 --- a/util/data/packed_rrset.c +++ b/util/data/packed_rrset.c @@ -198,6 +198,7 @@ get_cname_target(struct ub_packed_rrset_key* rrset, uint8_t** dname, { struct packed_rrset_data* d; size_t len; + if(!rrset) return; if(ntohs(rrset->rk.type) != LDNS_RR_TYPE_CNAME && ntohs(rrset->rk.type) != LDNS_RR_TYPE_DNAME) return; From 96f875552023c0ccf376ebe050519f12f3371dc9 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:18:16 +0200 Subject: [PATCH 21/26] - Fix CVE-2026-55973, 'dns-error-reporting: yes' leads to stack buffer overflow. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- services/cache/dns.c | 2 ++ services/mesh.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/cache/dns.c b/services/cache/dns.c index 98d07bc37..9fc79dad0 100644 --- a/services/cache/dns.c +++ b/services/cache/dns.c @@ -277,6 +277,8 @@ find_closest_of_type(struct module_env* env, uint8_t* qname, size_t qnamelen, /* snip off front label */ lablen = *qname; + if(lablen == 0) + break; qname += lablen + 1; qnamelen -= lablen + 1; } diff --git a/services/mesh.c b/services/mesh.c index 8ce3dcee7..5051de9df 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -1654,9 +1654,9 @@ static void dns_error_reporting(struct module_qstate* qstate, opt = edns_opt_list_find(qstate->edns_opts_back_in, LDNS_EDNS_REPORT_CHANNEL); if(!opt) return; - agent_domain_len = opt->opt_len; agent_domain = opt->opt_data; - if(dname_valid(agent_domain, agent_domain_len) < 3) { + agent_domain_len = dname_valid(agent_domain, opt->opt_len); + if(agent_domain_len < 3) { /* The agent domain needs to be a valid dname that is not the * root; from RFC9567. */ return; From ae1b3810cc3a8eb9b43c378289f74020b42aa7f4 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:18:41 +0200 Subject: [PATCH 22/26] - Fix CVE-2026-55990, Packet of death for a DNSCrypt misconfigured Unbound. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- dnscrypt/dnscrypt.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dnscrypt/dnscrypt.c b/dnscrypt/dnscrypt.c index 08f9dcb8c..6f1a236ad 100644 --- a/dnscrypt/dnscrypt.c +++ b/dnscrypt/dnscrypt.c @@ -672,6 +672,8 @@ dnsc_find_cert(struct dnsc_env* dnscenv, struct sldns_buffer* buffer) } dnscrypt_header = (struct dnscrypt_query_header *)sldns_buffer_begin(buffer); for (i = 0U; i < dnscenv->signed_certs_count; i++) { + if(!certs[i].keypair) + continue; if (memcmp(certs[i].magic_query, dnscrypt_header->magic_query, DNSCRYPT_MAGIC_HEADER_LEN) == 0) { return &certs[i]; @@ -813,6 +815,7 @@ dnsc_parse_keys(struct dnsc_env *env, struct config_file *cfg) sizeof *env->keypairs); env->certs = sodium_allocarray(env->signed_certs_count, sizeof *env->certs); + memset(env->certs, 0, env->signed_certs_count * sizeof(*env->certs)); cert_id = 0U; keypair_id = 0U; @@ -973,12 +976,19 @@ dnsc_create(void) int dnsc_apply_cfg(struct dnsc_env *env, struct config_file *cfg) { + int nkeys; if(dnsc_parse_certs(env, cfg) <= 0) { fatal_exit("dnsc_apply_cfg: no cert file loaded"); } - if(dnsc_parse_keys(env, cfg) <= 0) { + nkeys = dnsc_parse_keys(env, cfg); + if(nkeys <= 0) { fatal_exit("dnsc_apply_cfg: no key file loaded"); } + if((size_t)nkeys < env->signed_certs_count) { + fatal_exit("dnsc_apply_cfg: %u dnscrypt-provider-cert file(s) have no " + "matching dnscrypt-secret-key", + (unsigned)(env->signed_certs_count - (size_t)nkeys)); + } randombytes_buf(env->hash_key, sizeof env->hash_key); env->provider_name = cfg->dnscrypt_provider; From aac261cbb3795cbd60af2f37ef57bfa5c186aae6 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:19:02 +0200 Subject: [PATCH 23/26] - Fix CVE-2026-55991, Remote DNS-over-QUIC (DoQ) flow-control assertion failure in libngtcp2. Thanks to Qifan Zhang, Palo Alto Networks, for the report. In addition, thanks to Xuanchao Xie, for also reporting this issue. --- services/listen_dnsport.c | 46 +++++++++++++++++++++++++++------------ testcode/doqclient.c | 4 ++-- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/services/listen_dnsport.c b/services/listen_dnsport.c index 0500bff8d..38b493a25 100644 --- a/services/listen_dnsport.c +++ b/services/listen_dnsport.c @@ -4508,6 +4508,29 @@ doq_stream_reset_cb(ngtcp2_conn* ATTR_UNUSED(conn), int64_t stream_id, return 0; } +/** ngtcp2 extend_max_stream_data function */ +int doq_extend_max_stream_data_cb(ngtcp2_conn* ATTR_UNUSED(conn), + int64_t stream_id, uint64_t max_data, void* user_data, + void* ATTR_UNUSED(stream_user_data)) +{ + struct doq_conn* doq_conn = (struct doq_conn*)user_data; + struct doq_stream* stream; + verbose(VERB_ALGO, "doq extend_max_stream_data stream id %d " + "max_data %d ", (int)stream_id, (int)max_data); + if(max_data == 0) + return 0; + stream = doq_stream_find(doq_conn, stream_id); + if(!stream) { + verbose(VERB_ALGO, "doq: unknown stream %d", (int)stream_id); + return 0; + } + if(!stream->is_answer_available) + return 0; + doq_stream_on_write_list(doq_conn, stream); + doq_conn_write_enable(doq_conn); + return 0; +} + /** ngtcp2 acked_stream_data_offset callback function */ static int doq_acked_stream_data_offset_cb(ngtcp2_conn* ATTR_UNUSED(conn), @@ -4882,6 +4905,7 @@ doq_conn_setup(struct doq_conn* conn, uint8_t* scid, size_t scidlen, callbacks.stream_open = doq_stream_open_cb; callbacks.stream_close = doq_stream_close_cb; callbacks.stream_reset = doq_stream_reset_cb; + callbacks.extend_max_stream_data = doq_extend_max_stream_data_cb; callbacks.acked_stream_data_offset = doq_acked_stream_data_offset_cb; callbacks.recv_stream_data = doq_recv_stream_data_cb; @@ -5470,26 +5494,20 @@ doq_conn_write_streams(struct comm_point* c, struct doq_conn* conn, continue; } else if(ret == NGTCP2_ERR_STREAM_DATA_BLOCKED) { verbose(VERB_ALGO, "doq: ngtcp2_conn_writev_stream returned NGTCP2_ERR_STREAM_DATA_BLOCKED"); -#ifdef HAVE_NGTCP2_CCERR_DEFAULT - ngtcp2_ccerr_set_application_error( - &conn->ccerr, -1, NULL, 0); -#else - ngtcp2_connection_close_error_set_application_error(&conn->last_error, -1, NULL, 0); -#endif - if(err_drop) - *err_drop = 0; - if(!doq_conn_close_error(c, conn)) { - if(err_drop) - *err_drop = 1; + if(stream) { + doq_stream_off_write_list(conn, stream); + stream = stream->write_next; + continue; + } else { + break; } - return 0; } else if(ret == NGTCP2_ERR_STREAM_SHUT_WR) { verbose(VERB_ALGO, "doq: ngtcp2_conn_writev_stream returned NGTCP2_ERR_STREAM_SHUT_WR"); #ifdef HAVE_NGTCP2_CCERR_DEFAULT ngtcp2_ccerr_set_application_error( - &conn->ccerr, -1, NULL, 0); + &conn->ccerr, DOQ_APP_ERROR_CODE, NULL, 0); #else - ngtcp2_connection_close_error_set_application_error(&conn->last_error, -1, NULL, 0); + ngtcp2_connection_close_error_set_application_error(&conn->last_error, DOQ_APP_ERROR_CODE, NULL, 0); #endif if(err_drop) *err_drop = 0; diff --git a/testcode/doqclient.c b/testcode/doqclient.c index ce4d3417d..4c2142211 100644 --- a/testcode/doqclient.c +++ b/testcode/doqclient.c @@ -1519,9 +1519,9 @@ doq_client_send_pkt(struct doq_client_data* data, uint32_t ecn, uint8_t* buf, } log_err("doq sendmsg: %s", strerror(errno)); #ifdef HAVE_NGTCP2_CCERR_DEFAULT - ngtcp2_ccerr_set_application_error(&data->ccerr, -1, NULL, 0); + ngtcp2_ccerr_set_application_error(&data->ccerr, 1, NULL, 0); #else - ngtcp2_connection_close_error_set_application_error(&data->last_error, -1, NULL, 0); + ngtcp2_connection_close_error_set_application_error(&data->last_error, 1, NULL, 0); #endif return 0; } From 4b1635e19406fd8040806f0fa4a4488c003b5d0d Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:19:28 +0200 Subject: [PATCH 24/26] - Fix CVE-2026-56416, Possible heap buffer overflow when validator canonicalizes RDATA that contains domain name. Thanks to Qifan Zhang, Palo Alto Networks, for the report. --- util/data/msgparse.c | 3 +++ validator/val_sigcrypt.c | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/util/data/msgparse.c b/util/data/msgparse.c index 7b2de102e..4be1a72dc 100644 --- a/util/data/msgparse.c +++ b/util/data/msgparse.c @@ -687,6 +687,9 @@ calc_size(sldns_buffer* pkt, uint16_t type, struct rr_parse* rr) } rdf++; } + /* rdata ended before all _dname_count names were seen */ + if(count != 0) + return 0; /* the rdata is too short. */ } /* remaining rdata */ rr->size += pkt_len; diff --git a/validator/val_sigcrypt.c b/validator/val_sigcrypt.c index 46e6ac16b..16c01d2ee 100644 --- a/validator/val_sigcrypt.c +++ b/validator/val_sigcrypt.c @@ -1094,6 +1094,7 @@ canonicalize_rdata(sldns_buffer* buf, struct ub_packed_rrset_key* rrset, size_t len) { uint8_t* datstart = sldns_buffer_current(buf)-len+2; + size_t firstlen; switch(ntohs(rrset->rk.type)) { case LDNS_RR_TYPE_NXT: case LDNS_RR_TYPE_NS: @@ -1113,8 +1114,9 @@ canonicalize_rdata(sldns_buffer* buf, struct ub_packed_rrset_key* rrset, case LDNS_RR_TYPE_SOA: /* two names after another */ query_dname_tolower(datstart); - query_dname_tolower(datstart + - dname_valid(datstart, len-2)); + firstlen = dname_valid(datstart, len-2); + if(firstlen && firstlen < len-2) + query_dname_tolower(datstart + firstlen); return; case LDNS_RR_TYPE_RT: case LDNS_RR_TYPE_AFSDB: @@ -1141,8 +1143,9 @@ canonicalize_rdata(sldns_buffer* buf, struct ub_packed_rrset_key* rrset, return; datstart += 2; query_dname_tolower(datstart); - query_dname_tolower(datstart + - dname_valid(datstart, len-2-2)); + firstlen = dname_valid(datstart, len-2-2); + if(firstlen && firstlen < len-2-2) + query_dname_tolower(datstart + firstlen); return; case LDNS_RR_TYPE_NAPTR: if(len < 2+4) From 84d9682dd0876bc0cd118ecce03661f3443b0222 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:19:50 +0200 Subject: [PATCH 25/26] - Fix CVE-2026-56444, Degradation of resolution service when 'discard-timeout' and 'serve-expired-client-timeout' are combined in unusual configuration. Thanks to Qifan Zhang, Palo Alto Networks, for the report. In addition, thanks to Xin Wang, Jiapeng Li, and Jiajia Liu, Northwestern Polytechnical University, for also reporting this issue. In addition, thanks to Haruki Oyama (Waseda University), for also reporting this issue. --- services/mesh.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/services/mesh.c b/services/mesh.c index 5051de9df..99361b17f 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -2568,9 +2568,10 @@ mesh_serve_expired_callback(void* arg) log_dns_msg("Serve expired lookup", &qstate->qinfo, msg->rep); for(r = mstate->reply_list; r; r = r->next) { - struct timeval old; - timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time); - if(mstate->s.env->cfg->discard_timeout != 0 && + if(mesh_is_udp(r)) { + struct timeval old; + timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time); + if(mstate->s.env->cfg->discard_timeout != 0 && ((int)old.tv_sec)*1000+((int)old.tv_usec)/1000 > mstate->s.env->cfg->discard_timeout) { /* Drop the reply, it is too old */ @@ -2590,8 +2591,11 @@ mesh_serve_expired_callback(void* arg) 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++; continue; + } } i++; From c33ad1b1a21e48bfe4f140e90252a9be1b0c5745 Mon Sep 17 00:00:00 2001 From: "W.C.A. Wijngaards" Date: Wed, 22 Jul 2026 10:21:21 +0200 Subject: [PATCH 26/26] rerun autoconf. --- configure | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/configure b/configure index fd1e143e5..564f74afd 100755 --- a/configure +++ b/configure @@ -22923,6 +22923,29 @@ printf "%s\n" "no" >&6; } fi rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + ac_fn_check_decl "$LINENO" "CLOCK_MONOTONIC + " "ac_cv_have_decl_CLOCK_MONOTONIC_________" "$ac_includes_default +#ifdef TIME_WITH_SYS_TIME +# include +# include +#else +# ifdef HAVE_SYS_TIME_H +# include +# else +# include +# endif +#endif + +" "$ac_c_undeclared_builtin_options" "CFLAGS" +if test "x$ac_cv_have_decl_CLOCK_MONOTONIC_________" = xyes +then : + + +else $as_nop + as_fn_error $? "ngtcp2 for QUIC needs at least CLOCK_MONOTONIC on the system" "$LINENO" 5 + +fi + fi # set static linking for uninstalled libraries if requested