untrusted comment: verify with openbsd-77-base.pub
RWSbCCUoGpcxVZOGiVrEzI8009vkNXarn29T8M6Pqk7STVvcRyLAB1LjsS/x6pvlPZsvwpPaZpZHfGKQQR2377fC8UcYnBo1ewU=

OpenBSD 7.7 errata 017, December 3, 2025:

Fix incomplete mitigation of DNS cache poisoning vulnerabilities
in unbound.  CVE-2025-11411

Apply by doing:
    signify -Vep /etc/signify/openbsd-77-base.pub -x 017_unbound.patch.sig \
        -m - | (cd /usr/src && patch -p0)

And then rebuild and install unwind and unbound:
    cd /usr/src/sbin/unwind
    make obj
    make
    make install
    cd /usr/src/usr.sbin/unbound
    make -f Makefile.bsd-wrapper obj
    make -f Makefile.bsd-wrapper
    make -f Makefile.bsd-wrapper install

Index: sbin/unwind/libunbound/iterator/iter_scrub.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/iterator/iter_scrub.c,v
diff -u -p -r1.8.4.1 iter_scrub.c
--- sbin/unwind/libunbound/iterator/iter_scrub.c	23 Oct 2025 21:17:51 -0000	1.8.4.1
+++ sbin/unwind/libunbound/iterator/iter_scrub.c	28 Nov 2025 10:45:17 -0000
@@ -418,12 +418,13 @@ shorten_rrset(sldns_buffer* pkt, struct 
  * @param qinfo: original query.
  * @param region: where to allocate synthesized CNAMEs.
  * @param env: module env with config options.
+ * @param zonename: name of server zone.
  * @return 0 on error.
  */
 static int
 scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, 
 	struct query_info* qinfo, struct regional* region,
-	struct module_env* env)
+	struct module_env* env, uint8_t* zonename)
 {
 	uint8_t* sname = qinfo->qname;
 	size_t snamelen = qinfo->qname_len;
@@ -431,7 +432,8 @@ scrub_normalize(sldns_buffer* pkt, struc
 	int cname_length = 0; /* number of CNAMEs, or DNAMEs */
 
 	if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR &&
-		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN)
+		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN &&
+		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_YXDOMAIN)
 		return 1;
 
 	/* For the ANSWER section, remove all "irrelevant" records and add
@@ -470,6 +472,11 @@ scrub_normalize(sldns_buffer* pkt, struc
 				&aliaslen, pkt)) {
 				verbose(VERB_ALGO, "synthesized CNAME "
 					"too long");
+				if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) {
+					prev = rrset;
+					rrset = rrset->rrset_all_next;
+					continue;
+				}
 				return 0;
 			}
 			cname_length++;
@@ -650,6 +657,29 @@ scrub_normalize(sldns_buffer* pkt, struc
 					"RRset:", pkt, msg, prev, &rrset);
 				continue;
 			}
+			/* Also delete promiscuous NS for other RCODEs */
+			if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR
+				/* && env->cfg->iter_scrub_promiscuous */) {
+				remove_rrset("normalize: removing promiscuous "
+					"RRset:", pkt, msg, prev, &rrset);
+				continue;
+			}
+			/* Also delete promiscuous NS for NOERROR with nodata
+			 * for authoritative answers, not for delegations.
+			 * NOERROR with an_rrsets!=0 already handled.
+			 * Also NOERROR and soa_in_auth already handled.
+			 * NOERROR with an_rrsets==0, and not a referral.
+			 * referral is (NS not the zonename, noSOA).
+			 */
+			if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR
+				&& msg->an_rrsets == 0
+				&& !(dname_pkt_compare(pkt, rrset->dname,
+				     zonename) != 0 && !soa_in_auth(msg))
+				/* && env->cfg->iter_scrub_promiscuous */) {
+				remove_rrset("normalize: removing promiscuous "
+					"RRset:", pkt, msg, prev, &rrset);
+				continue;
+			}
 			if(nsset == NULL) {
 				nsset = rrset;
 			} else {
@@ -1060,7 +1090,8 @@ scrub_message(sldns_buffer* pkt, struct 
 	/* this is not required for basic operation but is a forgery 
 	 * resistance (security) feature */
 	if((FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR ||
-		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN) &&
+		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN ||
+		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) &&
 		msg->qdcount == 0)
 		return 0;
 
@@ -1074,7 +1105,7 @@ scrub_message(sldns_buffer* pkt, struct 
 	}
 
 	/* normalize the response, this cleans up the additional.  */
-	if(!scrub_normalize(pkt, msg, qinfo, region, env))
+	if(!scrub_normalize(pkt, msg, qinfo, region, env, zonename))
 		return 0;
 	/* delete all out-of-zone information */
 	if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate))
Index: usr.sbin/unbound/iterator/iter_scrub.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/iterator/iter_scrub.c,v
diff -u -p -r1.17.4.1 iter_scrub.c
--- usr.sbin/unbound/iterator/iter_scrub.c	23 Oct 2025 21:17:51 -0000	1.17.4.1
+++ usr.sbin/unbound/iterator/iter_scrub.c	28 Nov 2025 10:45:17 -0000
@@ -418,12 +418,13 @@ shorten_rrset(sldns_buffer* pkt, struct 
  * @param qinfo: original query.
  * @param region: where to allocate synthesized CNAMEs.
  * @param env: module env with config options.
+ * @param zonename: name of server zone.
  * @return 0 on error.
  */
 static int
 scrub_normalize(sldns_buffer* pkt, struct msg_parse* msg, 
 	struct query_info* qinfo, struct regional* region,
-	struct module_env* env)
+	struct module_env* env, uint8_t* zonename)
 {
 	uint8_t* sname = qinfo->qname;
 	size_t snamelen = qinfo->qname_len;
@@ -431,7 +432,8 @@ scrub_normalize(sldns_buffer* pkt, struc
 	int cname_length = 0; /* number of CNAMEs, or DNAMEs */
 
 	if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR &&
-		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN)
+		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NXDOMAIN &&
+		FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_YXDOMAIN)
 		return 1;
 
 	/* For the ANSWER section, remove all "irrelevant" records and add
@@ -470,6 +472,11 @@ scrub_normalize(sldns_buffer* pkt, struc
 				&aliaslen, pkt)) {
 				verbose(VERB_ALGO, "synthesized CNAME "
 					"too long");
+				if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) {
+					prev = rrset;
+					rrset = rrset->rrset_all_next;
+					continue;
+				}
 				return 0;
 			}
 			cname_length++;
@@ -650,6 +657,29 @@ scrub_normalize(sldns_buffer* pkt, struc
 					"RRset:", pkt, msg, prev, &rrset);
 				continue;
 			}
+			/* Also delete promiscuous NS for other RCODEs */
+			if(FLAGS_GET_RCODE(msg->flags) != LDNS_RCODE_NOERROR
+				/* && env->cfg->iter_scrub_promiscuous */) {
+				remove_rrset("normalize: removing promiscuous "
+					"RRset:", pkt, msg, prev, &rrset);
+				continue;
+			}
+			/* Also delete promiscuous NS for NOERROR with nodata
+			 * for authoritative answers, not for delegations.
+			 * NOERROR with an_rrsets!=0 already handled.
+			 * Also NOERROR and soa_in_auth already handled.
+			 * NOERROR with an_rrsets==0, and not a referral.
+			 * referral is (NS not the zonename, noSOA).
+			 */
+			if(FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR
+				&& msg->an_rrsets == 0
+				&& !(dname_pkt_compare(pkt, rrset->dname,
+				     zonename) != 0 && !soa_in_auth(msg))
+				/* && env->cfg->iter_scrub_promiscuous */) {
+				remove_rrset("normalize: removing promiscuous "
+					"RRset:", pkt, msg, prev, &rrset);
+				continue;
+			}
 			if(nsset == NULL) {
 				nsset = rrset;
 			} else {
@@ -1060,7 +1090,8 @@ scrub_message(sldns_buffer* pkt, struct 
 	/* this is not required for basic operation but is a forgery 
 	 * resistance (security) feature */
 	if((FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NOERROR ||
-		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN) &&
+		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_NXDOMAIN ||
+		FLAGS_GET_RCODE(msg->flags) == LDNS_RCODE_YXDOMAIN) &&
 		msg->qdcount == 0)
 		return 0;
 
@@ -1074,7 +1105,7 @@ scrub_message(sldns_buffer* pkt, struct 
 	}
 
 	/* normalize the response, this cleans up the additional.  */
-	if(!scrub_normalize(pkt, msg, qinfo, region, env))
+	if(!scrub_normalize(pkt, msg, qinfo, region, env, zonename))
 		return 0;
 	/* delete all out-of-zone information */
 	if(!scrub_sanitize(pkt, msg, qinfo, zonename, env, ie, qstate))
