From: Stuart Henderson Subject: unbound 1.26.2 To: tech Date: Wed, 21 Oct 2026 13:55:58 +0100 https://github.com/NLnetLabs/unbound/releases/tag/release-1.26.2 ok? Index: configure =================================================================== RCS file: /cvs/src/usr.sbin/unbound/configure,v diff -u -p -r1.64 configure --- configure 21 Sep 2026 08:43:48 -0000 1.64 +++ configure 21 Oct 2026 12:55:21 -0000 @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for unbound 1.26.1. +# Generated by GNU Autoconf 2.71 for unbound 1.26.2. # # Report bugs to . # @@ -622,8 +622,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='unbound' PACKAGE_TARNAME='unbound' -PACKAGE_VERSION='1.26.1' -PACKAGE_STRING='unbound 1.26.1' +PACKAGE_VERSION='1.26.2' +PACKAGE_STRING='unbound 1.26.2' PACKAGE_BUGREPORT='unbound-bugs@nlnetlabs.nl or https://github.com/NLnetLabs/unbound/issues' PACKAGE_URL='' @@ -1516,7 +1516,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures unbound 1.26.1 to adapt to many kinds of systems. +\`configure' configures unbound 1.26.2 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1582,7 +1582,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of unbound 1.26.1:";; + short | recursive ) echo "Configuration of unbound 1.26.2:";; esac cat <<\_ACEOF @@ -1835,7 +1835,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -unbound configure 1.26.1 +unbound configure 1.26.2 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2492,7 +2492,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by unbound $as_me 1.26.1, which was +It was created by unbound $as_me 1.26.2, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw @@ -3256,11 +3256,11 @@ UNBOUND_VERSION_MAJOR=1 UNBOUND_VERSION_MINOR=26 -UNBOUND_VERSION_MICRO=1 +UNBOUND_VERSION_MICRO=2 LIBUNBOUND_CURRENT=9 -LIBUNBOUND_REVISION=40 +LIBUNBOUND_REVISION=41 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -3368,6 +3368,7 @@ LIBUNBOUND_AGE=1 # 1.25.2 had 9:38:1 # 1.26.0 had 9:39:1 # 1.26.1 had 9:40:1 +# 1.26.2 had 9:41:1 # Current -- the number of the binary API that we're implementing # Revision -- which iteration of the implementation of the binary @@ -26467,7 +26468,7 @@ printf "%s\n" "#define MAXSYSLOGMSGLEN 1 -version=1.26.1 +version=1.26.2 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for build time" >&5 printf %s "checking for build time... " >&6; } @@ -26997,7 +26998,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_wri # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by unbound $as_me 1.26.1, which was +This file was extended by unbound $as_me 1.26.2, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -27065,7 +27066,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -unbound config.status 1.26.1 +unbound config.status 1.26.2 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" Index: configure.ac =================================================================== RCS file: /cvs/src/usr.sbin/unbound/configure.ac,v diff -u -p -r1.64 configure.ac --- configure.ac 21 Sep 2026 08:43:48 -0000 1.64 +++ configure.ac 21 Oct 2026 12:55:21 -0000 @@ -12,14 +12,14 @@ sinclude(dnscrypt/dnscrypt.m4) # must be numbers. ac_defun because of later processing m4_define([VERSION_MAJOR],[1]) m4_define([VERSION_MINOR],[26]) -m4_define([VERSION_MICRO],[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=40 +LIBUNBOUND_REVISION=41 LIBUNBOUND_AGE=1 # 1.0.0 had 0:12:0 # 1.0.1 had 0:13:0 @@ -127,6 +127,7 @@ LIBUNBOUND_AGE=1 # 1.25.2 had 9:38:1 # 1.26.0 had 9:39:1 # 1.26.1 had 9:40:1 +# 1.26.2 had 9:41:1 # Current -- the number of the binary API that we're implementing # Revision -- which iteration of the implementation of the binary Index: doc/README =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/README,v diff -u -p -r1.48 README --- doc/README 21 Sep 2026 08:43:48 -0000 1.48 +++ doc/README 21 Oct 2026 12:55:21 -0000 @@ -1,4 +1,4 @@ -README for Unbound 1.26.1 +README for Unbound 1.26.2 Copyright 2007 NLnet Labs http://unbound.net Index: doc/example.conf.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/example.conf.in,v diff -u -p -r1.53 example.conf.in --- doc/example.conf.in 21 Sep 2026 08:43:48 -0000 1.53 +++ doc/example.conf.in 21 Oct 2026 12:55:21 -0000 @@ -1,7 +1,7 @@ # # Example configuration file. # -# See unbound.conf(5) man page, version 1.26.1. +# See unbound.conf(5) man page, version 1.26.2. # # this is a comment. Index: doc/libunbound.3.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/libunbound.3.in,v diff -u -p -r1.51 libunbound.3.in --- doc/libunbound.3.in 21 Sep 2026 08:43:48 -0000 1.51 +++ doc/libunbound.3.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "LIBUNBOUND" "3" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "LIBUNBOUND" "3" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -libunbound \- Unbound DNS validating resolver 1.26.1 functions. +libunbound \- Unbound DNS validating resolver 1.26.2 functions. .SH SYNOPSIS .sp \fB#include \fP Index: doc/unbound-anchor.8.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-anchor.8.in,v diff -u -p -r1.50 unbound-anchor.8.in --- doc/unbound-anchor.8.in 21 Sep 2026 08:43:48 -0000 1.50 +++ doc/unbound-anchor.8.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND-ANCHOR" "8" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND-ANCHOR" "8" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound-anchor \- Unbound 1.26.1 anchor utility. +unbound-anchor \- Unbound 1.26.2 anchor utility. .SH SYNOPSIS .sp \fBunbound\-anchor\fP [\fBopts\fP] Index: doc/unbound-checkconf.8.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-checkconf.8.in,v diff -u -p -r1.50 unbound-checkconf.8.in --- doc/unbound-checkconf.8.in 21 Sep 2026 08:43:48 -0000 1.50 +++ doc/unbound-checkconf.8.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND-CHECKCONF" "8" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND-CHECKCONF" "8" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound-checkconf \- Check Unbound 1.26.1 configuration file for errors. +unbound-checkconf \- Check Unbound 1.26.2 configuration file for errors. .SH SYNOPSIS .sp \fBunbound\-checkconf\fP [\fB\-hf\fP] [\fB\-o option\fP] [cfgfile] Index: doc/unbound-control.8.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-control.8.in,v diff -u -p -r1.52 unbound-control.8.in --- doc/unbound-control.8.in 21 Sep 2026 08:43:48 -0000 1.52 +++ doc/unbound-control.8.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND-CONTROL" "8" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND-CONTROL" "8" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound-control \- Unbound 1.26.1 remote server control utility. +unbound-control \- Unbound 1.26.2 remote server control utility. .SH SYNOPSIS .sp \fBunbound\-control\fP [\fB\-hq\fP] [\fB\-c cfgfile\fP] [\fB\-s server\fP] command Index: doc/unbound-host.1.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound-host.1.in,v diff -u -p -r1.52 unbound-host.1.in --- doc/unbound-host.1.in 21 Sep 2026 08:43:48 -0000 1.52 +++ doc/unbound-host.1.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND-HOST" "1" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND-HOST" "1" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound-host \- Unbound 1.26.1 DNS lookup utility. +unbound-host \- Unbound 1.26.2 DNS lookup utility. .SH SYNOPSIS .sp \fBunbound\-host\fP [\fB\-C configfile\fP] [\fB\-vdhr46D\fP] [\fB\-c class\fP] Index: doc/unbound.8.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound.8.in,v diff -u -p -r1.53 unbound.8.in --- doc/unbound.8.in 21 Sep 2026 08:43:48 -0000 1.53 +++ doc/unbound.8.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND" "8" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND" "8" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound \- Unbound DNS validating resolver 1.26.1. +unbound \- Unbound DNS validating resolver 1.26.2. .SH SYNOPSIS .sp \fBunbound\fP [\fB\-hdpVv\fP] [\fB\-c \fP] Index: doc/unbound.conf.5.in =================================================================== RCS file: /cvs/src/usr.sbin/unbound/doc/unbound.conf.5.in,v diff -u -p -r1.58 unbound.conf.5.in --- doc/unbound.conf.5.in 21 Sep 2026 08:43:48 -0000 1.58 +++ doc/unbound.conf.5.in 21 Oct 2026 12:55:21 -0000 @@ -27,9 +27,9 @@ level margin: \\n[rst2man-indent\\n[rst2 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "UNBOUND.CONF" "5" "Sep 16, 2026" "1.26.1" "Unbound" +.TH "UNBOUND.CONF" "5" "Oct 21, 2026" "1.26.2" "Unbound" .SH NAME -unbound.conf \- Unbound 1.26.1 configuration file. +unbound.conf \- Unbound 1.26.2 configuration file. .SH SYNOPSIS .sp \fBunbound.conf\fP Index: services/authzone.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/services/authzone.c,v diff -u -p -r1.34 authzone.c --- services/authzone.c 21 Sep 2026 08:43:48 -0000 1.34 +++ services/authzone.c 21 Oct 2026 12:55:21 -0000 @@ -98,6 +98,11 @@ #define NUM_TIMEOUTS_FALLBACK_IXFR 3 /** number of IXFRs before an AXFR is performed, to consolidate RPZ memory. */ #define NUM_IXFR_BEFORE_AXFR 5 +/** The max number of decompression pointers that are allowed in a received + * xfr packet. The parse fails if the limit is exceeded. + * For a lot of PTR records, with compression for a deep zone, that is about + * 20x pointers per RR, of about 16 bytes, more than 4000*20 = 80000. */ +#define AUTH_XFR_DECOMPRESS_PACKET_LIMIT 128000 /** pick up nextprobe task to start waiting to perform transfer actions */ static void xfr_set_timeout(struct auth_xfer* xfr, struct module_env* env, @@ -1363,7 +1368,8 @@ az_remove_rr(struct auth_zone* z, uint8_ static int decompress_rr_into_buffer(struct sldns_buffer* buf, uint8_t* pkt, size_t pktlen, uint8_t* dname, uint16_t rr_type, uint16_t rr_class, - uint32_t rr_ttl, uint8_t* rr_data, uint16_t rr_rdlen) + uint32_t rr_ttl, uint8_t* rr_data, uint16_t rr_rdlen, int* budget, + int limit) { sldns_buffer pktbuf; size_t dname_len = 0; @@ -1377,7 +1383,7 @@ decompress_rr_into_buffer(struct sldns_b /* decompress dname */ sldns_buffer_set_position(&pktbuf, (size_t)(dname - sldns_buffer_current(&pktbuf))); - dname_len = pkt_dname_len(&pktbuf); + dname_len = pkt_dname_len_budget(&pktbuf, budget, limit); if(dname_len == 0) return 0; /* parse fail on dname */ if(!sldns_buffer_available(buf, dname_len)) return 0; dname_pkt_copy(&pktbuf, sldns_buffer_current(buf), dname); @@ -1412,7 +1418,8 @@ decompress_rr_into_buffer(struct sldns_b /* moves pktbuf to right after the * compressed dname, and returns uncompressed * dname length */ - uncompressed_len = pkt_dname_len(&pktbuf); + uncompressed_len = pkt_dname_len_budget( + &pktbuf, budget, limit); if(!uncompressed_len) return 0; /* parse error in dname */ compressed_len = sldns_buffer_position( @@ -1471,13 +1478,13 @@ static int az_insert_rr_decompress(struct auth_zone* z, uint8_t* pkt, size_t pktlen, struct sldns_buffer* scratch_buffer, uint8_t* dname, uint16_t rr_type, uint16_t rr_class, uint32_t rr_ttl, uint8_t* rr_data, - uint16_t rr_rdlen, int* duplicate) + uint16_t rr_rdlen, int* duplicate, int* budget, int limit) { uint8_t* rr; size_t rr_len; size_t dname_len; if(!decompress_rr_into_buffer(scratch_buffer, pkt, pktlen, dname, - rr_type, rr_class, rr_ttl, rr_data, rr_rdlen)) { + rr_type, rr_class, rr_ttl, rr_data, rr_rdlen, budget, limit)) { log_err("could not decompress RR"); return 0; } @@ -1493,13 +1500,13 @@ static int az_remove_rr_decompress(struct auth_zone* z, uint8_t* pkt, size_t pktlen, struct sldns_buffer* scratch_buffer, uint8_t* dname, uint16_t rr_type, uint16_t rr_class, uint32_t rr_ttl, uint8_t* rr_data, - uint16_t rr_rdlen, int* nonexist) + uint16_t rr_rdlen, int* nonexist, int* budget, int limit) { uint8_t* rr; size_t rr_len; size_t dname_len; if(!decompress_rr_into_buffer(scratch_buffer, pkt, pktlen, dname, - rr_type, rr_class, rr_ttl, rr_data, rr_rdlen)) { + rr_type, rr_class, rr_ttl, rr_data, rr_rdlen, budget, limit)) { log_err("could not decompress RR"); return 0; } @@ -4873,7 +4880,7 @@ chunk_rrlist_end(struct auth_chunk* rr_c /** RR list iterator, move to next RR */ static void chunk_rrlist_gonext(struct auth_chunk** rr_chunk, int* rr_num, - size_t* rr_pos, size_t rr_nextpos) + size_t* rr_pos, size_t rr_nextpos, int* budget) { /* already at end of chunks? */ if(!*rr_chunk) @@ -4887,6 +4894,7 @@ chunk_rrlist_gonext(struct auth_chunk** } /* no more RRs in this chunk */ /* continue with next chunk, see if it has RRs */ + *budget = 0; /* new budget for next packet */ if(*rr_chunk) *rr_chunk = (*rr_chunk)->next; while(*rr_chunk) { @@ -4970,11 +4978,12 @@ static int ixfr_start_serial(struct auth_chunk* rr_chunk, int rr_num, size_t rr_pos, uint8_t* rr_dname, uint16_t rr_type, uint16_t rr_class, uint32_t rr_ttl, uint16_t rr_rdlen, uint8_t* rr_rdata, - size_t rr_nextpos, uint32_t transfer_serial, uint32_t xfr_serial) + size_t rr_nextpos, uint32_t transfer_serial, uint32_t xfr_serial, + int* budget) { uint32_t startserial; /* move forward on RR */ - chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos); + chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos, budget); if(chunk_rrlist_end(rr_chunk, rr_num)) { /* no second SOA */ verbose(VERB_OPS, "IXFR has no second SOA record"); @@ -5031,6 +5040,7 @@ apply_ixfr(struct auth_xfer* xfr, struct size_t rr_counter = 0; int delmode = 0; int softfail = 0; + int budget = 0, limit = AUTH_XFR_DECOMPRESS_PACKET_LIMIT; xfr->num_ixfrs++; @@ -5066,7 +5076,8 @@ apply_ixfr(struct auth_xfer* xfr, struct if(!ixfr_start_serial(rr_chunk, rr_num, rr_pos, rr_dname, rr_type, rr_class, rr_ttl, rr_rdlen, rr_rdata, rr_nextpos, - transfer_serial, xfr->serial)) { + transfer_serial, xfr->serial, + &budget)) { return 0; } } else if(transfer_serial == serial) { @@ -5110,7 +5121,7 @@ apply_ixfr(struct auth_xfer* xfr, struct if(!az_remove_rr_decompress(z, rr_chunk->data, rr_chunk->len, scratch_buffer, rr_dname, rr_type, rr_class, rr_ttl, rr_rdata, rr_rdlen, - &nonexist)) { + &nonexist, &budget, limit)) { /* failed, malloc error or so */ return 0; } @@ -5130,7 +5141,7 @@ apply_ixfr(struct auth_xfer* xfr, struct if(!az_insert_rr_decompress(z, rr_chunk->data, rr_chunk->len, scratch_buffer, rr_dname, rr_type, rr_class, rr_ttl, rr_rdata, rr_rdlen, - &duplicate)) { + &duplicate, &budget, limit)) { /* failed, malloc error or so */ return 0; } @@ -5144,7 +5155,8 @@ apply_ixfr(struct auth_xfer* xfr, struct } rr_counter++; - chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos); + chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos, + &budget); } if(softfail) { verbose(VERB_ALGO, "IXFR did not apply cleanly, fetching full zone"); @@ -5168,6 +5180,7 @@ apply_axfr(struct auth_xfer* xfr, struct size_t rr_nextpos; size_t rr_counter = 0; int have_end_soa = 0; + int budget = 0, limit = AUTH_XFR_DECOMPRESS_PACKET_LIMIT; auth_zone_clear_data(z); xfr->have_zone = 0; @@ -5201,13 +5214,14 @@ apply_axfr(struct auth_xfer* xfr, struct /* add this RR */ if(!az_insert_rr_decompress(z, rr_chunk->data, rr_chunk->len, scratch_buffer, rr_dname, rr_type, rr_class, rr_ttl, - rr_rdata, rr_rdlen, NULL)) { + rr_rdata, rr_rdlen, NULL, &budget, limit)) { /* failed, malloc error or so */ return 0; } rr_counter++; - chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos); + chunk_rrlist_gonext(&rr_chunk, &rr_num, &rr_pos, rr_nextpos, + &budget); } if(!have_end_soa) { log_err("no end SOA record for AXFR"); @@ -6049,7 +6063,7 @@ check_xfer_packet(sldns_buffer* pkt, str int* gonextonfail, int* transferdone) { uint8_t* wire = sldns_buffer_begin(pkt); - int i; + int i, budget = 0, limit = AUTH_XFR_DECOMPRESS_PACKET_LIMIT; if(sldns_buffer_limit(pkt) < LDNS_HEADER_SIZE) { verbose(VERB_ALGO, "xfr to %s failed, packet too small", xfr->task_transfer->master->host); @@ -6111,7 +6125,7 @@ check_xfer_packet(sldns_buffer* pkt, str for(i=0; i<(int)LDNS_QDCOUNT(wire); i++) { size_t pos = sldns_buffer_position(pkt); uint16_t qtype, qclass; - if(pkt_dname_len(pkt) == 0) { + if(pkt_dname_len_budget(pkt, &budget, limit) == 0) { verbose(VERB_ALGO, "xfr to %s failed, packet with " "malformed dname", xfr->task_transfer->master->host); @@ -6161,7 +6175,7 @@ check_xfer_packet(sldns_buffer* pkt, str for(i=0; i<(int)LDNS_ANCOUNT(wire); i++) { size_t pos = sldns_buffer_position(pkt); uint16_t tp, rdlen; - if(pkt_dname_len(pkt) == 0) { + if(pkt_dname_len_budget(pkt, &budget, limit) == 0) { verbose(VERB_ALGO, "xfr to %s failed, packet with " "malformed dname in answer section", xfr->task_transfer->master->host); @@ -6277,7 +6291,7 @@ check_xfer_packet(sldns_buffer* pkt, str /* we skip over the RRs checking packet format */ for(i=0; i<(int)LDNS_NSCOUNT(wire); i++) { uint16_t rdlen; - if(pkt_dname_len(pkt) == 0) { + if(pkt_dname_len_budget(pkt, &budget, limit) == 0) { verbose(VERB_ALGO, "xfr to %s failed, packet with " "malformed dname in authority section", xfr->task_transfer->master->host); @@ -6306,7 +6320,7 @@ check_xfer_packet(sldns_buffer* pkt, str /* check additional section */ for(i=0; i<(int)LDNS_ARCOUNT(wire); i++) { uint16_t rdlen; - if(pkt_dname_len(pkt) == 0) { + if(pkt_dname_len_budget(pkt, &budget, limit) == 0) { verbose(VERB_ALGO, "xfr to %s failed, packet with " "malformed dname in additional section", xfr->task_transfer->master->host); Index: services/mesh.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/services/mesh.c,v diff -u -p -r1.36 mesh.c --- services/mesh.c 21 Sep 2026 08:43:48 -0000 1.36 +++ services/mesh.c 21 Oct 2026 12:55:21 -0000 @@ -506,8 +506,10 @@ void mesh_new_client(struct mesh_area* m "incoming query."); if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); +#ifdef HAVE_NGTCP2 else if(rep->c->type == comm_doq && rep->doq_stream) - doq_stream_remove_mesh_state(rep->doq_stream); + doq_find_stream_remove_mesh_state(rep); +#endif comm_point_drop_reply(rep); mesh->stats_dropped++; return; @@ -521,8 +523,10 @@ void mesh_new_client(struct mesh_area* m "dropping incoming query."); if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); +#ifdef HAVE_NGTCP2 else if(rep->c->type == comm_doq && rep->doq_stream) - doq_stream_remove_mesh_state(rep->doq_stream); + doq_find_stream_remove_mesh_state(rep); +#endif comm_point_drop_reply(rep); mesh->num_queries_replyaddr_limit++; return; @@ -598,6 +602,9 @@ void mesh_new_client(struct mesh_area* m 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) { + /* The mesh_new_client call is in the worker callback, that + * is called from the doq handler that has the conn locked + * for this stream. It can add directly, without more locks. */ doq_stream_add_meshstate(rep->doq_stream, mesh, s); } /* add serve expired timer if required and not already there */ @@ -655,6 +662,9 @@ servfail_mem: if(rep->c->use_h2) http2_stream_remove_mesh_state(rep->c->h2_stream); else if(rep->c->type == comm_doq && rep->doq_stream) + /* The mesh_new_client call is in the worker callback, that + * is called from the doq handler that has the conn locked + * for this stream. It can remove without more locks. */ doq_stream_remove_mesh_state(rep->doq_stream); comm_point_send_reply(rep); if(added_reply_without_accounting) { @@ -1145,9 +1155,10 @@ mesh_state_cleanup(struct mesh_state* ms mstate); else if(rep->query_reply.c->use_h2) http2_stream_remove_mesh_state(rep->h2_stream); +#ifdef HAVE_NGTCP2 else if(rep->query_reply.doq_stream) - doq_stream_remove_mesh_state( - rep->query_reply.doq_stream); + doq_find_stream_remove_mesh_state(&rep->query_reply); +#endif comm_point_drop_reply(&rep->query_reply); log_assert(mesh->num_reply_addrs > 0); mesh->num_reply_addrs--; @@ -1538,7 +1549,7 @@ mesh_send_reply(struct mesh_state* m, in r->h2_stream->mesh_state = NULL; #ifdef HAVE_NGTCP2 } else if(r->query_reply.doq_stream) { - r->query_reply.doq_stream->mesh_state = NULL; + doq_find_stream_remove_mesh_state(&r->query_reply); #endif } /* send the reply */ @@ -1837,8 +1848,10 @@ void mesh_query_done(struct mesh_state* mstate); else if(r->query_reply.c->use_h2) http2_stream_remove_mesh_state(r->h2_stream); +#ifdef HAVE_NGTCP2 else if(r->query_reply.doq_stream) - doq_stream_remove_mesh_state(r->query_reply.doq_stream); + doq_find_stream_remove_mesh_state(&r->query_reply); +#endif comm_point_drop_reply(&r->query_reply); log_assert(mstate->s.env->mesh->num_reply_addrs > 0); mstate->s.env->mesh->num_reply_addrs--; @@ -1871,8 +1884,10 @@ void mesh_query_done(struct mesh_state* r->query_reply.c->tcp_req_info, mstate); } else if(r->query_reply.c->use_h2) { http2_stream_remove_mesh_state(r->h2_stream); +#ifdef HAVE_NGTCP2 } else if(r->query_reply.doq_stream) { - doq_stream_remove_mesh_state(r->query_reply.doq_stream); + doq_find_stream_remove_mesh_state(&r->query_reply); +#endif } comm_point_drop_reply(&r->query_reply); log_assert(mstate->s.env->mesh->num_reply_addrs > 0); @@ -2481,8 +2496,11 @@ void mesh_state_remove_reply(struct mesh * 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; + /* When doq calls with doq_stream nonNULL, it removes + * the mesh_reply itself, and also already has the conn + * locked, so no need to remove it here */ + if(!doq_stream && n->query_reply.doq_stream) + doq_find_stream_remove_mesh_state(&n->query_reply); #endif /* prev = prev; */ @@ -2663,8 +2681,10 @@ mesh_serve_expired_callback(void* arg) r->query_reply.c->tcp_req_info, mstate); else if(r->query_reply.c->use_h2) http2_stream_remove_mesh_state(r->h2_stream); +#ifdef HAVE_NGTCP2 else if(r->query_reply.doq_stream) - doq_stream_remove_mesh_state(r->query_reply.doq_stream); + doq_find_stream_remove_mesh_state(&r->query_reply); +#endif comm_point_drop_reply(&r->query_reply); log_assert(mstate->s.env->mesh->num_reply_addrs > 0); mstate->s.env->mesh->num_reply_addrs--; Index: services/outside_network.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/services/outside_network.c,v diff -u -p -r1.36 outside_network.c --- services/outside_network.c 21 Sep 2026 08:43:48 -0000 1.36 +++ services/outside_network.c 21 Oct 2026 12:55:21 -0000 @@ -1524,6 +1524,49 @@ outnet_send_wait_udp(struct outside_netw } } +/** Clear out all pending requests that are on the port. */ +static void +clear_pending_from_port(struct outside_network* outnet, struct comm_point* c) +{ + /* Otherwise, the clear the cache action of unwanted_threshold, can + * have revealed this communication port. */ + struct pending* p, *np, *list = NULL, *last = NULL; + /* First, remove the entries from the tree. */ + /* The rbtree rbnode.right is used to build the temporary list here. + * Otherwise the list could be malloced. */ + p = (struct pending*)rbtree_first(outnet->pending); + while((rbnode_type*)p != RBTREE_NULL) { + np = (struct pending*)rbtree_next((rbnode_type*)p); + if(p->pc->cp == c) { + comm_timer_disable(p->timer); + (void)rbtree_delete(outnet->pending, p->node.key); + if(last) + last->node.right = (rbnode_type*)p; + else list = p; + last = p; + p->node.right = NULL; + } + p = np; + } + + /* The entries are removed from the tree, call their callbacks. */ + p = list; + while(p) { + np = (struct pending*)p->node.right; + p->node.right = RBTREE_NULL; + /* callback error on pending */ + if(p->cb) { + fptr_ok(fptr_whitelist_pending_udp(p->cb)); + (void)(*p->cb)(p->pc->cp, p->cb_arg, + NETEVENT_CLOSED, NULL); + } + portcomm_loweruse(outnet, p->pc); + pending_delete(NULL, p); + outnet_send_wait_udp(outnet); + p = np; + } +} + int outnet_udp_cb(struct comm_point* c, void* arg, int error, struct comm_reply *reply_info) @@ -1568,6 +1611,7 @@ outnet_udp_cb(struct comm_point* c, void outnet->unwanted_action)); (*outnet->unwanted_action)(outnet->unwanted_param); outnet->unwanted_total = 0; + clear_pending_from_port(outnet, c); } return 0; } @@ -1588,6 +1632,7 @@ outnet_udp_cb(struct comm_point* c, void outnet->unwanted_action)); (*outnet->unwanted_action)(outnet->unwanted_param); outnet->unwanted_total = 0; + clear_pending_from_port(outnet, c); } return 0; } Index: testcode/fake_event.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/testcode/fake_event.c,v diff -u -p -r1.1.1.21 fake_event.c --- testcode/fake_event.c 21 Sep 2026 08:42:30 -0000 1.1.1.21 +++ testcode/fake_event.c 21 Oct 2026 12:55:21 -0000 @@ -2046,6 +2046,10 @@ void doq_stream_remove_mesh_state(struct { } +void doq_find_stream_remove_mesh_state(struct comm_reply* ATTR_UNUSED(repinfo)) +{ +} + void fast_reload_service_cb(int ATTR_UNUSED(fd), short ATTR_UNUSED(event), void* ATTR_UNUSED(arg)) { Index: testcode/unitdname.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/testcode/unitdname.c,v diff -u -p -r1.1.1.4 unitdname.c --- testcode/unitdname.c 26 Sep 2025 07:30:48 -0000 1.1.1.4 +++ testcode/unitdname.c 21 Oct 2026 12:55:21 -0000 @@ -269,6 +269,55 @@ dname_test_pkt_dname_len(sldns_buffer* b unit_assert( pkt_dname_len(buff) == 0 ); } +/** test pkt_dname_len_budget */ +static void +dname_test_pkt_dname_len_budget(sldns_buffer* buff) +{ + int budget = 0, limit = 20; + unit_show_func("util/data/dname.c", "pkt_dname_len_budget"); + + sldns_buffer_clear(buff); + sldns_buffer_write(buff, "\000", 1); + sldns_buffer_flip(buff); + unit_assert( pkt_dname_len_budget(buff, &budget, limit) == 1 ); + unit_assert( sldns_buffer_position(buff) == 1); + unit_assert( budget == 0); + + sldns_buffer_clear(buff); + sldns_buffer_write(buff, "\007example\003com\000", 13); + sldns_buffer_flip(buff); + unit_assert( pkt_dname_len_budget(buff, &budget, limit) == 13 ); + unit_assert( sldns_buffer_position(buff) == 13); + unit_assert( budget == 0); + + sldns_buffer_clear(buff); + sldns_buffer_skip(buff, 12); /* skip header */ + sldns_buffer_write(buff, "\007example\003com\000", 13); + sldns_buffer_skip(buff, 10); /* more packet content */ + sldns_buffer_write(buff, "\003www", 4); + sldns_buffer_write_u8(buff, 0xc0); + sldns_buffer_write_u8(buff, 0x0c); + sldns_buffer_flip(buff); + sldns_buffer_set_position(buff, 35); + unit_assert( pkt_dname_len_budget(buff, &budget, limit) == 17 ); + unit_assert( sldns_buffer_position(buff) == 41); + unit_assert( budget == 1); + + sldns_buffer_clear(buff); + sldns_buffer_skip(buff, 12); /* skip header */ + sldns_buffer_write(buff, "\001a", 2); + sldns_buffer_write_u8(buff, 0xc0); + sldns_buffer_write_u8(buff, 0x0c); + sldns_buffer_skip(buff, 10); /* more packet content */ + sldns_buffer_write(buff, "\003www", 4); + sldns_buffer_write_u8(buff, 0xc0); + sldns_buffer_write_u8(buff, 0x0c); + sldns_buffer_flip(buff); + sldns_buffer_set_position(buff, 26); + unit_assert( pkt_dname_len_budget(buff, &budget, limit) == 0 ); + unit_assert( budget == 21); +} + /** test dname_lab_cmp */ static void dname_test_dname_lab_cmp(void) @@ -1296,6 +1345,7 @@ void dname_test(void) dname_test_count_size_labels(); dname_test_dname_lab_cmp(); dname_test_pkt_dname_len(buff); + dname_test_pkt_dname_len_budget(buff); dname_test_strict_subdomain(); dname_test_subdomain(); dname_test_isroot(); Index: util/netevent.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/util/netevent.c,v diff -u -p -r1.45 netevent.c --- util/netevent.c 21 Sep 2026 08:43:49 -0000 1.45 +++ util/netevent.c 21 Oct 2026 12:55:21 -0000 @@ -2828,7 +2828,7 @@ doq_lookup_repinfo(struct doq_table* tab /** doq find connection and stream. From inside callbacks from worker. */ static int doq_lookup_conn_stream(struct comm_reply* repinfo, struct comm_point* c, - struct doq_conn** conn, struct doq_stream** stream) + struct doq_conn** conn, struct doq_stream** stream, int checkclose) { log_assert(c->doq_socket); if(c->doq_socket->current_conn) { @@ -2856,7 +2856,7 @@ doq_lookup_conn_stream(struct comm_reply } return 0; } - if((*stream)->is_closed) { + if(checkclose && (*stream)->is_closed) { /* stream is closed, ignore reply or drop */ if(!c->doq_socket->current_conn) { /* Not inside callbacks, we have our own lock on conn. @@ -2868,6 +2868,24 @@ doq_lookup_conn_stream(struct comm_reply return 1; } +/** doq remove mesh state from stream */ +void doq_find_stream_remove_mesh_state(struct comm_reply* repinfo) +{ + struct doq_conn* conn; + struct doq_stream* stream; + log_assert(repinfo->c->type == comm_doq); + if(!doq_lookup_conn_stream(repinfo, repinfo->c, &conn, &stream, 0)) { + /* No stream, it may have been removed. */ + return; + } + doq_stream_remove_mesh_state(stream); + if(!repinfo->c->doq_socket->current_conn) { + /* Not inside callbacks, we have our own lock on conn. + * Release it. */ + lock_basic_unlock(&conn->lock); + } +} + /** doq send a reply from a comm reply */ static void doq_socket_send_reply(struct comm_reply* repinfo) @@ -2875,7 +2893,7 @@ doq_socket_send_reply(struct comm_reply* struct doq_conn* conn; struct doq_stream* stream; log_assert(repinfo->c->type == comm_doq); - if(!doq_lookup_conn_stream(repinfo, repinfo->c, &conn, &stream)) { + if(!doq_lookup_conn_stream(repinfo, repinfo->c, &conn, &stream, 1)) { verbose(VERB_ALGO, "doq: send_reply but %s is gone", (conn?"stream":"connection")); /* No stream, it may have been closed. */ @@ -2906,7 +2924,7 @@ doq_socket_drop_reply(struct comm_reply* struct doq_conn* conn; struct doq_stream* stream; log_assert(repinfo->c->type == comm_doq); - if(!doq_lookup_conn_stream(repinfo, repinfo->c, &conn, &stream)) { + if(!doq_lookup_conn_stream(repinfo, repinfo->c, &conn, &stream, 1)) { verbose(VERB_ALGO, "doq: drop_reply but %s is gone", (conn?"stream":"connection")); /* The connection or stream is already gone. */ Index: util/netevent.h =================================================================== RCS file: /cvs/src/usr.sbin/unbound/util/netevent.h,v diff -u -p -r1.28 netevent.h --- util/netevent.h 21 Sep 2026 08:43:49 -0000 1.28 +++ util/netevent.h 21 Oct 2026 12:55:21 -0000 @@ -1136,6 +1136,10 @@ void doq_send_pkt(struct comm_point* c, /** doq timer callback function. */ void doq_timer_cb(void* arg); +/** Find and lock doq connection and remove mesh state from the stream + * (found by id).*/ +void doq_find_stream_remove_mesh_state(struct comm_reply* repinfo); + /** tcp read again callback function. For tcp req info listen. */ void tcp_read_again_cb(void* arg); Index: util/data/dname.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/util/data/dname.c,v diff -u -p -r1.10 dname.c --- util/data/dname.c 21 Sep 2026 08:43:49 -0000 1.10 +++ util/data/dname.c 21 Oct 2026 12:55:21 -0000 @@ -236,6 +236,61 @@ fail: return 0; } +size_t +pkt_dname_len_budget(struct sldns_buffer* pkt, int* budget, int limit) +{ + size_t len = 0; + int ptrcount = 0; + uint8_t labellen; + size_t endpos = 0; + + /* read dname and determine length */ + /* check compression pointers, loops, out of bounds */ + while(1) { + /* read next label */ + if(sldns_buffer_remaining(pkt) < 1) + return 0; + labellen = sldns_buffer_read_u8(pkt); + if(LABEL_IS_PTR(labellen)) { + /* compression ptr */ + uint16_t ptr; + if(sldns_buffer_remaining(pkt) < 1) + return 0; + ptr = PTR_OFFSET(labellen, sldns_buffer_read_u8(pkt)); + if(++(*budget) > limit) { + verbose(VERB_ALGO, "The decompression pointer " + "limit of %d has been exceeded", limit); + return 0; /* too many decompression pointers */ + } + if(ptrcount++ > MAX_COMPRESS_PTRS) + return 0; /* loop! */ + if(sldns_buffer_limit(pkt) <= ptr) + return 0; /* out of bounds! */ + if(!endpos) + endpos = sldns_buffer_position(pkt); + sldns_buffer_set_position(pkt, ptr); + } else { + /* label contents */ + if(labellen > 0x3f) + return 0; /* label too long */ + len += 1 + labellen; + if(len > LDNS_MAX_DOMAINLEN) + return 0; + if(labellen == 0) { + /* end of dname */ + break; + } + if(sldns_buffer_remaining(pkt) < labellen) + return 0; + sldns_buffer_skip(pkt, (ssize_t)labellen); + } + } + if(endpos) + sldns_buffer_set_position(pkt, endpos); + + return len; +} + int dname_pkt_compare(sldns_buffer* pkt, uint8_t* d1, uint8_t* d2) { Index: util/data/dname.h =================================================================== RCS file: /cvs/src/usr.sbin/unbound/util/data/dname.h,v diff -u -p -r1.8 dname.h --- util/data/dname.h 26 Sep 2025 07:32:37 -0000 1.8 +++ util/data/dname.h 21 Oct 2026 12:55:21 -0000 @@ -110,6 +110,22 @@ int query_dname_compare(uint8_t* d1, uin size_t pkt_dname_len(struct sldns_buffer* pkt); /** + * Determine correct, compressed, dname present in packet. + * Checks for parse errors. Has a budget and limit for decompression, + * if the limit is exceeded, this is an error. + * @param pkt: packet to read from (from current start position). + * @param budget: the budget of decompression pointer follows. + * It is updated with the additional count (if any) of pointer follows, + * so that a running total can be kept over a number of dnames. + * @param limit: the limit of decompression pointer follows. + * @return: 0 on parse error. + * At exit the position is right after the (compressed) dname. + * Compression pointers are followed and checked for loops. + * The uncompressed wireformat length is returned. + */ +size_t pkt_dname_len_budget(struct sldns_buffer* pkt, int* budget, int limit); + +/** * Compare dnames in packet (compressed). Dnames must be valid. * routine performs lowercasing, so the packet casing is preserved. * @param pkt: packet, used to resolve compression pointers. Index: util/data/msgparse.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/util/data/msgparse.c,v diff -u -p -r1.16 msgparse.c --- util/data/msgparse.c 21 Sep 2026 08:43:49 -0000 1.16 +++ util/data/msgparse.c 21 Oct 2026 12:55:21 -0000 @@ -54,6 +54,9 @@ #include "sldns/wire2str.h" #define MAX_PARSED_EDNS_OPTIONS 100 +/** The max number of decompression pointers that are allowed in a received + * packet. The parse fails if the limit is exceeded. */ +#define PARSE_DECOMPRESS_LIMIT 2048 /** smart comparison of (compressed, valid) dnames from packet */ static int @@ -114,7 +117,7 @@ new_rrset(struct msg_parse* msg, uint8_t /** See if next rrset is nsec at zone apex */ static int -nsec_at_apex(sldns_buffer* pkt) +nsec_at_apex(sldns_buffer* pkt, int* budget, int limit) { /* we are at ttl position in packet. */ size_t pos = sldns_buffer_position(pkt); @@ -128,7 +131,7 @@ nsec_at_apex(sldns_buffer* pkt) return 0; /* parse error happens later */ } /* must validate the nsec next domain name format */ - if(pkt_dname_len(pkt) == 0) { + if(pkt_dname_len_budget(pkt, budget, limit) == 0) { sldns_buffer_set_position(pkt, pos); return 0; /* parse error */ } @@ -159,10 +162,11 @@ nsec_at_apex(sldns_buffer* pkt) /** Calculate rrset flags */ static uint32_t -pkt_rrset_flags(sldns_buffer* pkt, uint16_t type, sldns_pkt_section sec) +pkt_rrset_flags(sldns_buffer* pkt, uint16_t type, sldns_pkt_section sec, + int* budget, int limit) { uint32_t f = 0; - if(type == LDNS_RR_TYPE_NSEC && nsec_at_apex(pkt)) { + if(type == LDNS_RR_TYPE_NSEC && nsec_at_apex(pkt, budget, limit)) { f |= PACKED_RRSET_NSEC_AT_APEX; } else if(type == LDNS_RR_TYPE_SOA && sec == LDNS_SECTION_AUTHORITY) { f |= PACKED_RRSET_SOA_NEG; @@ -462,6 +466,8 @@ change_rrsig_rrset(struct rrset_parse* s * @param rrset_prev: last seen RRset. * @param section: the current section in the packet. * @param region: used to allocate temporary parsing data. + * @param budget: decompression budget. + * @param limit: decompression limit. * @return 0 on out of memory. */ static int @@ -471,7 +477,8 @@ find_rrset(struct msg_parse* msg, sldns_ uint8_t** prev_dname_first, uint8_t** prev_dname_last, size_t* prev_dnamelen, uint16_t* prev_type, uint16_t* prev_dclass, struct rrset_parse** rrset_prev, - sldns_pkt_section section, struct regional* region) + sldns_pkt_section section, struct regional* region, int* budget, + int limit) { hashvalue_type dname_h = pkt_hash_rrset_first(pkt, dname); uint16_t covtype; @@ -498,7 +505,7 @@ find_rrset(struct msg_parse* msg, sldns_ } } /* find by hashing and lookup in hashtable */ - *rrset_flags = pkt_rrset_flags(pkt, type, section); + *rrset_flags = pkt_rrset_flags(pkt, type, section, budget, limit); /* if rrsig - try to lookup matching data set first */ if(type == LDNS_RR_TYPE_RRSIG && pkt_rrsig_covered(pkt, @@ -580,10 +587,13 @@ find_rrset(struct msg_parse* msg, sldns_ * @param pkt: packet, position at call must be at start of query section. * at end position is after query section. * @param msg: store results here. + * @param budget: decompression budget. + * @param limit: decompression limit. * @return: 0 if OK, or rcode on error. */ static int -parse_query_section(sldns_buffer* pkt, struct msg_parse* msg) +parse_query_section(sldns_buffer* pkt, struct msg_parse* msg, int* budget, + int limit) { if(msg->qdcount == 0) return 0; @@ -593,7 +603,7 @@ parse_query_section(sldns_buffer* pkt, s if(sldns_buffer_remaining(pkt) <= 0) return LDNS_RCODE_FORMERR; msg->qname = sldns_buffer_current(pkt); - if((msg->qname_len = pkt_dname_len(pkt)) == 0) + if((msg->qname_len = pkt_dname_len_budget(pkt, budget, limit)) == 0) return LDNS_RCODE_FORMERR; if(sldns_buffer_remaining(pkt) < sizeof(uint16_t)*2) return LDNS_RCODE_FORMERR; @@ -638,7 +648,8 @@ get_rdf_size(sldns_rdf_type rdf) /** calculate the size of one rr */ static int -calc_size(sldns_buffer* pkt, uint16_t type, struct rr_parse* rr) +calc_size(sldns_buffer* pkt, uint16_t type, struct rr_parse* rr, int* budget, + int limit) { const sldns_rr_descriptor* desc; uint16_t pkt_len; /* length of rr inside the packet */ @@ -659,7 +670,8 @@ calc_size(sldns_buffer* pkt, uint16_t ty case LDNS_RDF_TYPE_DNAME: /* decompress every domain name */ oldpos = sldns_buffer_position(pkt); - if((len = pkt_dname_len(pkt)) == 0) + if((len = pkt_dname_len_budget(pkt, budget, + limit)) == 0) return 0; /* malformed dname */ if(sldns_buffer_position(pkt)-oldpos > pkt_len) return 0; /* dname exceeds rdata */ @@ -758,7 +770,7 @@ sig_is_double(sldns_buffer* pkt, struct static int add_rr_to_rrset(struct rrset_parse* rrset, sldns_buffer* pkt, struct msg_parse* msg, struct regional* region, - sldns_pkt_section section, uint16_t type) + sldns_pkt_section section, uint16_t type, int* budget, int limit) { struct rr_parse* rr; /* check section of rrset. */ @@ -809,8 +821,10 @@ add_rr_to_rrset(struct rrset_parse* rrse } /* calc decompressed size */ - if(!calc_size(pkt, type, rr)) + if(!calc_size(pkt, type, rr, budget, limit)) { + if(*budget > limit) return LDNS_RCODE_SERVFAIL; return LDNS_RCODE_FORMERR; + } rrset->size += rr->size; return 0; @@ -825,12 +839,14 @@ add_rr_to_rrset(struct rrset_parse* rrse * @param section: section enum. * @param num_rrs: how many rrs are in the section. * @param num_rrsets: returns number of rrsets in the section. + * @param budget: decompression budget. + * @param limit: decompression limit. * @return: 0 if OK, or rcode on error. */ static int parse_section(sldns_buffer* pkt, struct msg_parse* msg, struct regional* region, sldns_pkt_section section, - uint16_t num_rrs, size_t* num_rrsets) + uint16_t num_rrs, size_t* num_rrsets, int* budget, int limit) { uint16_t i; uint8_t* dname, *prev_dname_f = NULL, *prev_dname_l = NULL; @@ -849,8 +865,10 @@ parse_section(sldns_buffer* pkt, struct for(i=0; i limit) return LDNS_RCODE_SERVFAIL; return LDNS_RCODE_FORMERR; + } if(sldns_buffer_remaining(pkt) < 10) /* type, class, ttl, len */ return LDNS_RCODE_FORMERR; type = sldns_buffer_read_u16(pkt); @@ -886,7 +904,7 @@ parse_section(sldns_buffer* pkt, struct if(!find_rrset(msg, pkt, dname, dnamelen, type, dclass, &hash, &rrset_flags, &prev_dname_f, &prev_dname_l, &prev_dnamelen, &prev_type, &prev_dclass, &rrset, - section, region)) + section, region, budget, limit)) return LDNS_RCODE_SERVFAIL; if(!rrset) { /* it is a new RR set. hash&flags already calculated.*/ @@ -906,7 +924,7 @@ parse_section(sldns_buffer* pkt, struct } /* add to rrset. */ if((r=add_rr_to_rrset(rrset, pkt, msg, region, section, - type)) != 0) + type, budget, limit)) != 0) return r; } return 0; @@ -915,7 +933,7 @@ parse_section(sldns_buffer* pkt, struct int parse_packet(sldns_buffer* pkt, struct msg_parse* msg, struct regional* region) { - int ret; + int ret, budget = 0, limit = PARSE_DECOMPRESS_LIMIT; if(sldns_buffer_remaining(pkt) < LDNS_HEADER_SIZE) return LDNS_RCODE_FORMERR; /* read the header */ @@ -927,19 +945,20 @@ parse_packet(sldns_buffer* pkt, struct m msg->arcount = sldns_buffer_read_u16(pkt); if(msg->qdcount > 1) return LDNS_RCODE_FORMERR; - if((ret = parse_query_section(pkt, msg)) != 0) + if((ret = parse_query_section(pkt, msg, &budget, limit)) != 0) return ret; if((ret = parse_section(pkt, msg, region, LDNS_SECTION_ANSWER, - msg->ancount, &msg->an_rrsets)) != 0) + msg->ancount, &msg->an_rrsets, &budget, limit)) != 0) return ret; if((ret = parse_section(pkt, msg, region, LDNS_SECTION_AUTHORITY, - msg->nscount, &msg->ns_rrsets)) != 0) + msg->nscount, &msg->ns_rrsets, &budget, limit)) != 0) return ret; if(sldns_buffer_remaining(pkt) == 0 && msg->arcount == 1) { /* BIND accepts leniently that an EDNS record is missing. * so, we do too. */ } else if((ret = parse_section(pkt, msg, region, - LDNS_SECTION_ADDITIONAL, msg->arcount, &msg->ar_rrsets)) != 0) + LDNS_SECTION_ADDITIONAL, msg->arcount, &msg->ar_rrsets, + &budget, limit)) != 0) return ret; /* if(sldns_buffer_remaining(pkt) > 0) { */ /* there is spurious data at end of packet. ignore */ Index: validator/validator.c =================================================================== RCS file: /cvs/src/usr.sbin/unbound/validator/validator.c,v diff -u -p -r1.31 validator.c --- validator/validator.c 21 Sep 2026 08:43:49 -0000 1.31 +++ validator/validator.c 21 Oct 2026 12:55:21 -0000 @@ -1963,14 +1963,7 @@ processInit(struct module_qstate* qstate lookup_name = vq->qchase.qname; lookup_len = vq->qchase.qname_len; /* for type DS look at the parent side for keys/trustanchor */ - /* also for NSEC not at apex */ - if(vq->qchase.qtype == LDNS_RR_TYPE_DS || - (vq->qchase.qtype == LDNS_RR_TYPE_NSEC && - vq->orig_msg->rep->rrset_count > vq->rrset_skip && - ntohs(vq->orig_msg->rep->rrsets[vq->rrset_skip]->rk.type) == - LDNS_RR_TYPE_NSEC && - !(vq->orig_msg->rep->rrsets[vq->rrset_skip]-> - rk.flags&PACKED_RRSET_NSEC_AT_APEX))) { + if(vq->qchase.qtype == LDNS_RR_TYPE_DS) { dname_remove_label(&lookup_name, &lookup_len); } @@ -2007,13 +2000,15 @@ processInit(struct module_qstate* qstate anchor = anchors_lookup(qstate->env->anchors, lookup_name, lookup_len, vq->qchase.qclass); if(!anchor) { /* unsigned parent denies anchor*/ - verbose(VERB_QUERY, "unsigned parent zone denies" - " trust anchor, indeterminate"); - vq->chase_reply->security = sec_status_indeterminate; - update_reason_bogus(vq->chase_reply, LDNS_EDE_DNSSEC_INDETERMINATE); + verbose(VERB_ALGO, "unsigned parent zone denies" + " trust anchor, thus bogus"); + errinf_ede(qstate, "unsigned parent zone denies trust anchor, thus bogus", LDNS_EDE_DNSSEC_BOGUS); + vq->chase_reply->security = sec_status_bogus; + update_reason_bogus(vq->chase_reply, LDNS_EDE_DNSSEC_BOGUS); vq->state = VAL_FINISHED_STATE; return 1; } + vq->parent_denial_must_be_secure = 1; verbose(VERB_ALGO, "trust anchor NXDOMAIN by signed parent"); } else if(subtype == VAL_CLASS_POSITIVE && qstate->qinfo.qtype == LDNS_RR_TYPE_DNSKEY && @@ -2538,6 +2533,15 @@ processFinished(struct module_qstate* qs enum val_classification subtype = val_classify_response( qstate->query_flags, &qstate->qinfo, &vq->qchase, vq->orig_msg->rep, vq->rrset_skip); + if(vq->parent_denial_must_be_secure) { + if(vq->chase_reply->security != sec_status_secure && + vq->chase_reply->security != sec_status_bogus) { + verbose(VERB_ALGO, "parent denial of trust island subdomain is not secure, thus bogus"); + vq->chase_reply->security = sec_status_bogus; + update_reason_bogus(vq->chase_reply, LDNS_EDE_DNSSEC_BOGUS); + errinf_ede(qstate, "parent denial of trust island subdomain is not secure, thus bogus", LDNS_EDE_DNSSEC_BOGUS); + } + } /* store overall validation result in orig_msg */ if(vq->rrset_skip == 0) { Index: validator/validator.h =================================================================== RCS file: /cvs/src/usr.sbin/unbound/validator/validator.h,v diff -u -p -r1.12 validator.h --- validator/validator.h 21 Sep 2026 08:43:49 -0000 1.12 +++ validator/validator.h 21 Oct 2026 12:55:21 -0000 @@ -244,6 +244,10 @@ struct val_qstate { int num_nsec_attempts; /** The nsec attempts have been exceeded. */ int num_nsec_attempts_exceeded; + + /** if validation must be secure, (else bogus) for NXDOMAIN + * from signed parent to (removed) trust anchor subdomain. */ + int parent_denial_must_be_secure; }; /**