Fix CVE-2026-10846: validate DNS response in ldns

Backport CVE-2026-10846 fix from upstream ldns 1.9.0 to 1.7.0.
The patch adds DNS response validation including source address
verification, transaction ID matching, query section count
validation, and response question matching against the original
query. New error status codes are added for specific failure
modes.

CVE: CVE-2026-10846
Upstream patches:
 - https://src.fedoraproject.org/rpms/ldns/raw/rawhide/f/ldns-1.9.0-CVE-2026-10846.patch
Resolves: RHEL-210700

This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.

Assisted-by: Ymir
This commit is contained in:
RHEL Packaging Agent 2026-07-15 09:29:33 +00:00
parent da4629d247
commit 1a0ca6d073
2 changed files with 211 additions and 1 deletions

View File

@ -0,0 +1,202 @@
From cc51382f8bf8bb5675e93ed16e816ccdfb1581ea Mon Sep 17 00:00:00 2001
From: RHEL Packaging Agent <redhat-ymir-agent@redhat.com>
Date: Wed, 15 Jul 2026 09:22:45 +0000
Subject: [PATCH] Fix CVE-2026-10846: validate DNS response source address and
query matching
Adapted from upstream patch for ldns 1.9.0. Adds:
- ldns_sockaddr_cmp() to verify response comes from queried address
- Transaction ID matching check
- Query section count validation
- Response question matching against original query
- New error status codes for ID mismatch, qdcount, and query mismatch
---
ldns-1.7.0_python3/error.c | 6 +++
ldns-1.7.0_python3/ldns/error.h | 5 +-
ldns-1.7.0_python3/net.c | 92 ++++++++++++++++++++++++++++++++-
3 files changed, 100 insertions(+), 3 deletions(-)
diff --git a/ldns-1.7.0_python3/error.c b/ldns-1.7.0_python3/error.c
index 35ee5bd..a3103f8 100644
--- a/ldns-1.7.0_python3/error.c
+++ b/ldns-1.7.0_python3/error.c
@@ -157,6 +157,12 @@ ldns_lookup_table ldns_error_str[] = {
"X509_STORE_CTX_set0_dane() functions within OpenSSL >= 1.1.0 "
"to be able to verify the DANE-TA usage type." },
#endif
+ { 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/ldns-1.7.0_python3/ldns/error.h b/ldns-1.7.0_python3/ldns/error.h
index 15f49a2..936df38 100644
--- a/ldns-1.7.0_python3/ldns/error.h
+++ b/ldns-1.7.0_python3/ldns/error.h
@@ -129,7 +129,10 @@ enum ldns_enum_status {
LDNS_STATUS_RDATA_OVERFLOW,
LDNS_STATUS_SYNTAX_SUPERFLUOUS_TEXT_ERR,
LDNS_STATUS_NSEC3_DOMAINNAME_OVERFLOW,
- LDNS_STATUS_DANE_NEED_OPENSSL_GE_1_1_FOR_DANE_TA
+ LDNS_STATUS_DANE_NEED_OPENSSL_GE_1_1_FOR_DANE_TA,
+ 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/ldns-1.7.0_python3/net.c b/ldns-1.7.0_python3/net.c
index 6e6a12b..53b4df5 100644
--- a/ldns-1.7.0_python3/net.c
+++ b/ldns-1.7.0_python3/net.c
@@ -410,6 +410,50 @@ ldns_udp_bgsend(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,
@@ -418,6 +462,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);
@@ -436,13 +482,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;
@@ -481,6 +535,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);
@@ -568,6 +626,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) {
@@ -577,7 +645,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;
@@ -637,6 +705,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;
}

View File

@ -39,7 +39,7 @@
Summary: Low-level DNS(SEC) library with API
Name: ldns
Version: 1.7.0
Release: 22%{?dist}
Release: 23%{?dist}
License: BSD
Url: http://www.nlnetlabs.nl/%{name}/
@ -48,6 +48,8 @@ Patch1: ldns-1.7.0-multilib.patch
Patch2: ldns-1.7.0-parse-limit.patch
Patch3: ldns-1.7.0-realloc.patch
Patch4: ldns-1.7.0-coverity.patch
# https://src.fedoraproject.org/rpms/ldns/raw/rawhide/f/ldns-1.9.0-CVE-2026-10846.patch
Patch5: ldns-1.7.0-CVE-2026-10846.patch
Group: System Environment/Libraries
# Only needed for builds from svn snapshot
@ -160,6 +162,7 @@ pushd %{pkgname}
%patch2 -p1 -b .limit
%patch3 -p1 -b .realloc
%patch4 -p1 -b .covscan
%patch5 -p2 -b .cve-2026-10846
# To built svn snapshots
%if 0%{snapshot}
rm config.guess config.sub ltmain.sh
@ -355,6 +358,11 @@ rm -rf doc/man
%doc doc
%changelog
* Wed Jul 15 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 1.7.0-23
- Fix CVE-2026-10846: validate DNS response source address, transaction
ID, and query section matching
- Resolves: RHEL-210700
* Tue Jan 16 2024 Petr Menšík <pemensik@redhat.com> - 1.7.0-22
- Export ldns-utils, ldns-doc, perl-ldns and python3-ldns into CRB (RHEL-315)