git: 5719a342555b - stable/14 - ldns: Fix query response validation

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Tue, 09 Jun 2026 19:17:55 UTC
The branch stable/14 has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=5719a342555bc56b552db27b1852193968b86323

commit 5719a342555bc56b552db27b1852193968b86323
Author:     Gordon Tetlow <gordon@FreeBSD.org>
AuthorDate: 2026-06-07 15:27:56 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2026-06-09 19:15:22 +0000

    ldns: Fix query response validation
    
    Approved by:    so
    Security:       FreeBSD-SA-26:36.ldns
    Security:       CVE-2026-10846
---
 contrib/ldns/error.c      |  6 ++++
 contrib/ldns/ldns/error.h |  5 ++-
 contrib/ldns/net.c        | 92 +++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 100 insertions(+), 3 deletions(-)

diff --git a/contrib/ldns/error.c b/contrib/ldns/error.c
index 5723aea9b4c2..4fc05d6d0d8f 100644
--- a/contrib/ldns/error.c
+++ b/contrib/ldns/error.c
@@ -191,6 +191,12 @@ ldns_lookup_table ldns_error_str[] = {
 		"at least 2 bytes of option data" },
 	{ LDNS_STATUS_EQUAL_RR,
 		"An identical RR already existed in the zone" },
+	{ LDNS_STATUS_ID_DID_NOT_MATCH,
+		"Response ID did not match the query ID" },
+	{ LDNS_STATUS_QDCOUNT_MUST_BE_ONE,
+		"The query section MUST contain exactly one question" },
+	{ LDNS_STATUS_QUERY_DID_NOT_MATCH,
+		"The question in the response did not match the query" },
 	{ 0, NULL }
 };
 
diff --git a/contrib/ldns/ldns/error.h b/contrib/ldns/ldns/error.h
index a76eb2ecab5d..41d64cc0815f 100644
--- a/contrib/ldns/ldns/error.h
+++ b/contrib/ldns/ldns/error.h
@@ -144,7 +144,10 @@ enum ldns_enum_status {
 	LDNS_STATUS_INVALID_SVCPARAM_VALUE,
 	LDNS_STATUS_NOT_EDE,
 	LDNS_STATUS_EDE_OPTION_MALFORMED,
-	LDNS_STATUS_EQUAL_RR
+	LDNS_STATUS_EQUAL_RR,
+	LDNS_STATUS_ID_DID_NOT_MATCH,
+	LDNS_STATUS_QDCOUNT_MUST_BE_ONE,
+	LDNS_STATUS_QUERY_DID_NOT_MATCH
 };
 typedef enum ldns_enum_status ldns_status;
 
diff --git a/contrib/ldns/net.c b/contrib/ldns/net.c
index e944d018b357..4c1f405419fb 100644
--- a/contrib/ldns/net.c
+++ b/contrib/ldns/net.c
@@ -441,6 +441,50 @@ ldns_udp_bgsend2(ldns_buffer *qbin,
 	return ldns_udp_bgsend_from(qbin, to, tolen, NULL, 0, timeout);
 }
 
+/** helper sockaddr compare function. returns -1, 0 or 1. */
+static int
+ldns_sockaddr_cmp(const struct sockaddr_storage* addr1, socklen_t len1,
+	const struct sockaddr_storage* addr2, socklen_t len2)
+{
+	struct sockaddr_in* p1_in = (struct sockaddr_in*)addr1;
+	struct sockaddr_in* p2_in = (struct sockaddr_in*)addr2;
+	struct sockaddr_in6* p1_in6 = (struct sockaddr_in6*)addr1;
+	struct sockaddr_in6* p2_in6 = (struct sockaddr_in6*)addr2;
+	if(len1 < len2)
+		return -1;
+	if(len1 > len2)
+		return 1;
+	assert(len1 == len2);
+	if( p1_in->sin_family < p2_in->sin_family)
+		return -1;
+	if( p1_in->sin_family > p2_in->sin_family)
+		return 1;
+	assert( p1_in->sin_family == p2_in->sin_family );
+	/* compare ip4 */
+	if( p1_in->sin_family == AF_INET ) {
+		/* just order it, ntohs not required */
+		if(p1_in->sin_port < p2_in->sin_port)
+			return -1;
+		if(p1_in->sin_port > p2_in->sin_port)
+			return 1;
+		assert(p1_in->sin_port == p2_in->sin_port);
+		return memcmp(&p1_in->sin_addr, &p2_in->sin_addr,
+			sizeof(p1_in->sin_addr));
+	} else if (p1_in6->sin6_family == AF_INET6) {
+		/* just order it, ntohs not required */
+		if(p1_in6->sin6_port < p2_in6->sin6_port)
+			return -1;
+		if(p1_in6->sin6_port > p2_in6->sin6_port)
+			return 1;
+		assert(p1_in6->sin6_port == p2_in6->sin6_port);
+		return memcmp(&p1_in6->sin6_addr, &p2_in6->sin6_addr,
+			sizeof(p1_in6->sin6_addr));
+	} else {
+		/* eek unknown type, perform this comparison for sanity. */
+		return memcmp(addr1, addr2, len1);
+	}
+}
+
 static ldns_status
 ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
 		const struct sockaddr_storage *to  , socklen_t tolen,
@@ -449,6 +493,8 @@ ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
 {
 	int sockfd;
 	uint8_t *answer;
+	struct sockaddr_storage reply_addr;
+	socklen_t reply_addr_len;
 
 	sockfd = ldns_udp_bgsend_from(qbin, to, tolen, from, fromlen, timeout);
 
@@ -467,13 +513,21 @@ ldns_udp_send_from(uint8_t **result, ldns_buffer *qbin,
          * but returns a 'NETWORK_ERROR' much like a timeout. */
         ldns_sock_nonblock(sockfd);
 
-	answer = ldns_udp_read_wire(sockfd, answer_size, NULL, NULL);
+	reply_addr_len = sizeof(reply_addr);
+	memset(&reply_addr, 0, reply_addr_len);
+	answer = ldns_udp_read_wire(sockfd, answer_size, &reply_addr,
+		&reply_addr_len);
 	close_socket(sockfd);
 
 	if (!answer) {
 		/* oops */
 		return LDNS_STATUS_NETWORK_ERR;
 	}
+	/* Check that the reply came from the to addr. */
+	if(ldns_sockaddr_cmp(to, tolen, &reply_addr, reply_addr_len) != 0) {
+		free(answer);
+		return LDNS_STATUS_NETWORK_ERR;
+	}
 
 	*result = answer;
 	return LDNS_STATUS_OK;
@@ -512,6 +566,10 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
 
 	assert(r != NULL);
 
+	/* The query should at least have one question */
+	if(ldns_buffer_limit(qb) < 6 || ldns_buffer_read_u16_at(qb, 4) != 1)
+		return LDNS_STATUS_QDCOUNT_MUST_BE_ONE;
+
 	status = LDNS_STATUS_OK;
 	rtt = ldns_resolver_rtt(r);
 	ns_array = ldns_resolver_nameservers(r);
@@ -599,6 +657,16 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
 			ldns_resolver_set_nameserver_rtt(r, i, LDNS_RESOLV_RTT_INF);
 			status = send_status;
 		}
+		if(reply_bytes && ldns_buffer_limit(qb) >= 2) {
+			uint16_t txid = ldns_buffer_read_u16_at(qb, 0);
+			if(reply_size < 2 ||
+				ldns_read_uint16(reply_bytes) != txid) {
+				status = LDNS_STATUS_ID_DID_NOT_MATCH;
+				LDNS_FREE(reply_bytes);
+				reply_bytes = NULL;
+				reply_size = 0;
+			}
+		}
 		
 		/* obey the fail directive */
 		if (!reply_bytes) {
@@ -608,7 +676,7 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
 					LDNS_FREE(src);
 				}
 				LDNS_FREE(ns);
-				return LDNS_STATUS_ERR;
+				return status ? status : LDNS_STATUS_ERR;
 			} else {
 				LDNS_FREE(ns);
 				continue;
@@ -670,6 +738,26 @@ ldns_send_buffer(ldns_pkt **result, ldns_resolver *r, ldns_buffer *qb, ldns_rdf
 #endif /* HAVE_SSL */
 
 	LDNS_FREE(reply_bytes);
+	if (reply) {
+		ldns_pkt *query = NULL;
+
+		if(ldns_pkt_qdcount(reply) != 1) {
+			status = LDNS_STATUS_QDCOUNT_MUST_BE_ONE;
+			ldns_pkt_free(reply);
+			reply = NULL;
+
+		} else if(ldns_wire2pkt(&query
+		                , ldns_buffer_begin(qb)
+		                , ldns_buffer_position(qb)) != LDNS_STATUS_OK
+		|| ldns_pkt_qdcount(query) != 1
+		|| ldns_rr_compare(ldns_rr_list_rr(ldns_pkt_question(query),0)
+		                  ,ldns_rr_list_rr(ldns_pkt_question(reply),0))){
+			status = LDNS_STATUS_QUERY_DID_NOT_MATCH;
+			ldns_pkt_free(reply);
+			reply = NULL;
+		}
+		ldns_pkt_free(query);
+	}
 	if (result) {
 		*result = reply;
 	}