package/libldns: add patch for CVE-2026-10846

This fixes the following vulnerability:
- CVE-2026-10846:
    NLnet Labs ldns 1.2.0 up to and including versions 1.9.0, when used in
    applications as (stub) resolver over UDP, lacks matching the query
    destination address and port with the response source address and
    port. Furthermore not the query ID, neither the question of the query
    is matched with that of the response. This makes applications, that
    use ldns for (stub) resolver functionality over UDP, vulnerable for
    off-path poisoning attacks. The drill tool, which is shipped with
    ldns, suffers from this vulnerability.
    https://www.cve.org/CVERecord?id=CVE-2026-10846
    https://www.nlnetlabs.nl/downloads/ldns/CVE-2026-10846.txt

Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
Signed-off-by: Raphaël Mélotte <raphael.melotte@mind.be>
This commit is contained in:
Titouan Christophe
2026-09-01 19:15:02 +02:00
committed by Raphaël Mélotte
parent 82eff78dea
commit 8e44aff085
2 changed files with 228 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
From f179ebe97b0c7309edb87a14d6f73119dbecb708 Mon Sep 17 00:00:00 2001
From: Titouan Christophe <titouan.christophe@mind.be>
Date: Tue, 1 Sep 2026 19:09:47 +0200
Subject: [PATCH] Fix CVE-2026-10846
== Summary
When ldns is used by applications for (stub) resolving, it does not
sufficiently verify that received responses belong to a sent query.
== Affected products
ldns 1.2.0 up to and including 1.9.0
== Description
NLnet Labs ldns 1.2.0 up to and including versions 1.9.0, when used in
applications as (stub) resolver over UDP, lacks matching the query destination
address and port with the response source address and port. Furthermore not the
query ID, neither the question of the query is matched with that of the
response. This makes applications, that use ldns for (stub) resolver
functionality, vulnerable for off-path poisoning attacks.
The drill tool, which is shipped with ldns and uses ldns for stub resolving,
inherently suffers from this vulnerability too.
== Solution
Download a patched version of ldns, or apply the patch manually
CVE: CVE-2026-10846
Upstream: https://nlnetlabs.nl/downloads/ldns/patch_cve_2026-10846.diff
Signed-off-by: Titouan Christophe <titouan.christophe@mind.be>
---
error.c | 8 +++++
ldns/error.h | 6 +++-
net.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/error.c b/error.c
index 50cdc630..4fc05d6d 100644
--- a/error.c
+++ b/error.c
@@ -189,6 +189,14 @@ ldns_lookup_table ldns_error_str[] = {
{ LDNS_STATUS_EDE_OPTION_MALFORMED,
"The extended error code option is malformed, expected "
"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/ldns/error.h b/ldns/error.h
index 011df284..41d64cc0 100644
--- a/ldns/error.h
+++ b/ldns/error.h
@@ -143,7 +143,11 @@ enum ldns_enum_status {
LDNS_STATUS_SVCPARAM_KEY_MORE_THAN_ONCE,
LDNS_STATUS_INVALID_SVCPARAM_VALUE,
LDNS_STATUS_NOT_EDE,
- LDNS_STATUS_EDE_OPTION_MALFORMED
+ LDNS_STATUS_EDE_OPTION_MALFORMED,
+ 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/net.c b/net.c
index 8fc9c8c4..100226d4 100644
--- a/net.c
+++ b/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;
}
--
2.55.0

View File

@@ -23,6 +23,9 @@ LIBLDNS_CONF_OPTS = \
--without-pyldns \
--without-pyldnsx
# 0001-fix-cve-2026-10846.patch
LIBLDNS_IGNORE_CVES += CVE-2026-10846
ifeq ($(BR2_PACKAGE_LIBOPENSSL),y)
LIBLDNS_CONF_OPTS += --enable-dane-verify
else