resolv: Correctly count records and check hostname validity (RHEL-168851, RHEL-168852)

Resolves: RHEL-168851
Resolves: RHEL-168852
This commit is contained in:
Arjun Shankar 2026-05-04 13:48:25 +02:00 committed by Florian Weimer
parent 437183f942
commit a35ccbc161
2 changed files with 552 additions and 0 deletions

234
glibc-RHEL-168851.patch Normal file
View File

@ -0,0 +1,234 @@
commit 5663ab0b833df187b15e7bb4b18173e22beb8bd1
Author: Carlos O'Donell <carlos@redhat.com>
Date: Fri Mar 20 16:43:33 2026 -0400
resolv: Count records correctly (CVE-2026-4437)
The answer section boundary was previously ignored, and the code in
getanswer_ptr would iterate past the last resource record, but not
beyond the end of the returned data. This could lead to subsequent data
being interpreted as answer records, thus violating the DNS
specification. Such resource records could be maliciously crafted and
hidden from other tooling, but processed by the glibc stub resolver and
acted upon by the application. While we trust the data returned by the
configured recursive resolvers, we should not trust its format and
should validate it as required. It is a security issue to incorrectly
process the DNS protocol.
A regression test is added for response section crossing.
No regressions on x86_64-linux-gnu.
Reviewed-by: Collin Funk <collin.funk1@gmail.com>
(cherry picked from commit 9f5f18aab40ec6b61fa49a007615e6077e9a979b)
diff --git a/resolv/Makefile b/resolv/Makefile
index 116ab6a1574eb1ff..649c1254c30d40c6 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -108,6 +108,7 @@ tests += \
tst-resolv-basic \
tst-resolv-binary \
tst-resolv-byaddr \
+ tst-resolv-dns-section \
tst-resolv-edns \
tst-resolv-invalid-cname \
tst-resolv-network \
@@ -119,6 +120,7 @@ tests += \
tst-resolv-semi-failure \
tst-resolv-short-response \
tst-resolv-trailing \
+ # tests
# This test calls __res_context_send directly, which is not exported
# from libresolv.
@@ -288,6 +290,8 @@ $(objpfx)tst-resolv-aliases: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-basic: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-binary: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-byaddr: $(objpfx)libresolv.so $(shared-thread-library)
+$(objpfx)tst-resolv-dns-section: $(objpfx)libresolv.so \
+ $(shared-thread-library)
$(objpfx)tst-resolv-edns: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-network: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-res_init: $(objpfx)libresolv.so
diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
index 3d261b6810bba5c9..54d6e2822d3454ca 100644
--- a/resolv/nss_dns/dns-host.c
+++ b/resolv/nss_dns/dns-host.c
@@ -821,7 +821,7 @@ getanswer_ptr (unsigned char *packet, size_t packetlen,
/* expected_name may be updated to point into this buffer. */
unsigned char name_buffer[NS_MAXCDNAME];
- while (ancount > 0)
+ for (; ancount > 0; --ancount)
{
struct ns_rr_wire rr;
if (!__ns_rr_cursor_next (&c, &rr))
diff --git a/resolv/tst-resolv-dns-section.c b/resolv/tst-resolv-dns-section.c
new file mode 100644
index 0000000000000000..1171baef51e3cc36
--- /dev/null
+++ b/resolv/tst-resolv-dns-section.c
@@ -0,0 +1,162 @@
+/* Test handling of invalid section transitions (bug 34014).
+ Copyright (C) 2022-2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <array_length.h>
+#include <errno.h>
+#include <netdb.h>
+#include <resolv.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/format_nss.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+
+/* Name of test, and the second section type. */
+struct item {
+ const char *test;
+ int ns_section;
+};
+
+static const struct item test_items[] =
+ {
+ { "Test crossing from ns_s_an to ns_s_ar.", ns_s_ar },
+ { "Test crossing from ns_s_an to ns_s_an.", ns_s_ns },
+
+ { NULL, 0 },
+ };
+
+/* The response is designed to contain the following:
+ - An Answer section with one T_PTR record that is skipped.
+ - A second section with a semantically invalid T_PTR record.
+ The original defect is that the response parsing would cross
+ section boundaries and handle the additional section T_PTR
+ as if it were an answer. A conforming implementation would
+ stop as soon as it reaches the end of the section. */
+static void
+response (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ TEST_COMPARE (qclass, C_IN);
+
+ /* We only test PTR. */
+ TEST_COMPARE (qtype, T_PTR);
+
+ unsigned int count;
+ char *tail = NULL;
+
+ if (strstr (qname, "in-addr.arpa") != NULL
+ && sscanf (qname, "%u.%ms", &count, &tail) == 2)
+ TEST_COMPARE_STRING (tail, "0.168.192.in-addr.arpa");
+ else if (sscanf (qname, "%x.%ms", &count, &tail) == 2)
+ {
+ TEST_COMPARE_STRING (tail, "\
+0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa");
+ }
+ else
+ FAIL_EXIT1 ("invalid QNAME: %s\n", qname);
+ free (tail);
+
+ /* We have a bounded number of possible tests. */
+ TEST_VERIFY (count >= 0);
+ TEST_VERIFY (count <= 15);
+
+ struct resolv_response_flags flags = {};
+ resolv_response_init (b, flags);
+ resolv_response_add_question (b, qname, qclass, qtype);
+ resolv_response_section (b, ns_s_an);
+
+ /* Actual answer record, but the wrong name (skipped). */
+ resolv_response_open_record (b, "1.0.0.10.in-addr.arpa", qclass, qtype, 60);
+
+ /* Record the answer. */
+ resolv_response_add_name (b, "test.ptr.example.net");
+ resolv_response_close_record (b);
+
+ /* Add a second section to test section boundary crossing. */
+ resolv_response_section (b, test_items[count].ns_section);
+ /* Semantically incorrect, but hide a T_PTR entry. */
+ resolv_response_open_record (b, qname, qclass, qtype, 60);
+ resolv_response_add_name (b, "wrong.ptr.example.net");
+ resolv_response_close_record (b);
+}
+
+
+/* Perform one check using a reverse lookup. */
+static void
+check_reverse (int af, int count)
+{
+ TEST_VERIFY (af == AF_INET || af == AF_INET6);
+ TEST_VERIFY (count < array_length (test_items));
+
+ char addr[sizeof (struct in6_addr)] = { 0 };
+ socklen_t addrlen;
+ if (af == AF_INET)
+ {
+ addr[0] = (char) 192;
+ addr[1] = (char) 168;
+ addr[2] = (char) 0;
+ addr[3] = (char) count;
+ addrlen = 4;
+ }
+ else
+ {
+ addr[0] = 0x20;
+ addr[1] = 0x01;
+ addr[2] = 0x0d;
+ addr[3] = 0xb8;
+ addr[4] = addr[5] = addr[6] = addr[7] = 0x0;
+ addr[8] = addr[9] = addr[10] = addr[11] = 0x0;
+ addr[12] = 0x0;
+ addr[13] = 0x0;
+ addr[14] = 0x0;
+ addr[15] = count;
+ addrlen = 16;
+ }
+
+ h_errno = 0;
+ struct hostent *answer = gethostbyaddr (addr, addrlen, af);
+ TEST_VERIFY (answer == NULL);
+ TEST_VERIFY (h_errno == NO_RECOVERY);
+ if (answer != NULL)
+ printf ("error: unexpected success: %s\n",
+ support_format_hostent (answer));
+}
+
+static int
+do_test (void)
+{
+ struct resolv_test *obj = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response
+ });
+
+ for (int i = 0; test_items[i].test != NULL; i++)
+ {
+ check_reverse (AF_INET, i);
+ check_reverse (AF_INET6, i);
+ }
+
+ resolv_test_end (obj);
+
+ return 0;
+}
+
+#include <support/test-driver.c>

318
glibc-RHEL-168852.patch Normal file
View File

@ -0,0 +1,318 @@
commit 9344c796f7a4ac8f2c59de63f3e1e936b51e817d
Author: Carlos O'Donell <carlos@redhat.com>
Date: Fri Mar 20 17:14:33 2026 -0400
resolv: Check hostname for validity (CVE-2026-4438)
The processed hostname in getanswer_ptr should be correctly checked to
avoid invalid characters from being allowed, including shell
metacharacters. It is a security issue to fail to check the returned
hostname for validity.
A regression test is added for invalid metacharacters and other cases
of invalid or valid characters.
No regressions on x86_64-linux-gnu.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
(cherry picked from commit e10977481f4db4b2a3ce34fa4c3a1e26651ae312)
Conflicts:
resolv/Makefile
(fixup context)
diff --git a/resolv/Makefile b/resolv/Makefile
index 649c1254c30d40c6..aa25c71f1eea9c28 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -111,6 +111,7 @@ tests += \
tst-resolv-dns-section \
tst-resolv-edns \
tst-resolv-invalid-cname \
+ tst-resolv-invalid-ptr \
tst-resolv-network \
tst-resolv-noaaaa \
tst-resolv-noaaaa-vc \
@@ -302,6 +303,8 @@ $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
$(objpfx)tst-resolv-invalid-cname: $(objpfx)libresolv.so \
$(shared-thread-library)
$(objpfx)tst-resolv-no-search: $(objpfx)libresolv.so $(shared-thread-library)
+$(objpfx)tst-resolv-invalid-ptr: $(objpfx)libresolv.so \
+ $(shared-thread-library)
$(objpfx)tst-resolv-noaaaa: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-noaaaa-vc: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-nondecimal: $(objpfx)libresolv.so $(shared-thread-library)
diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
index 54d6e2822d3454ca..2016131b24a55145 100644
--- a/resolv/nss_dns/dns-host.c
+++ b/resolv/nss_dns/dns-host.c
@@ -867,7 +867,7 @@ getanswer_ptr (unsigned char *packet, size_t packetlen,
char hname[MAXHOSTNAMELEN + 1];
if (__ns_name_unpack (c.begin, c.end, rr.rdata,
name_buffer, sizeof (name_buffer)) < 0
- || !__res_binary_hnok (expected_name)
+ || !__res_binary_hnok (name_buffer)
|| __ns_name_ntop (name_buffer, hname, sizeof (hname)) < 0)
{
*h_errnop = NO_RECOVERY;
diff --git a/resolv/tst-resolv-invalid-ptr.c b/resolv/tst-resolv-invalid-ptr.c
new file mode 100644
index 0000000000000000..0c802ab96780efb0
--- /dev/null
+++ b/resolv/tst-resolv-invalid-ptr.c
@@ -0,0 +1,255 @@
+/* Test handling of invalid T_PTR results (bug 34015).
+ Copyright (C) 2022-2026 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <array_length.h>
+#include <errno.h>
+#include <netdb.h>
+#include <resolv.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/format_nss.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+
+/* Name of test, the answer, the expected error return, and if we
+ expect the call to fail. */
+struct item {
+ const char *test;
+ const char *answer;
+ int expected;
+ bool fail;
+};
+
+static const struct item test_items[] =
+ {
+ /* Test for invalid characters. */
+ { "Invalid use of \"|\"",
+ "test.|.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"&\"",
+ "test.&.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \";\"",
+ "test.;.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"<\"",
+ "test.<.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \">\"",
+ "test.>.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"(\"",
+ "test.(.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \")\"",
+ "test.).ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"$\"",
+ "test.$.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"`\"",
+ "test.`.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\\\"",
+ "test.\\.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\'\"",
+ "test.'.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\"\"",
+ "test.\".ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \" \"",
+ "test. .ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\\t\"",
+ "test.\t.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\\n\"",
+ "test.\n.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"\\r\"",
+ "test.\r.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"*\"",
+ "test.*.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"?\"",
+ "test.?.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"[\"",
+ "test.[.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"]\"",
+ "test.].ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \",\"",
+ "test.,.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"~\"",
+ "test.~.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \":\"",
+ "test.:.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"!\"",
+ "test.!.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"@\"",
+ "test.@.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"#\"",
+ "test.#.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"%\"",
+ "test.%%.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of \"^\"",
+ "test.^.ptr.example", NO_RECOVERY, true },
+
+ /* Test for invalid UTF-8 characters (2-byte, 4-byte, 6-byte). */
+ { "Invalid use of UTF-8 (2-byte, U+00C0-U+00C2)",
+ "ÁÂÃ.test.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of UTF-8 (4-byte, U+0750-U+0752)",
+ "ݐݑݒ.test.ptr.example", NO_RECOVERY, true },
+ { "Invalid use of UTF-8 (6-byte, U+0904-U+0906)",
+ "ऄअआ.test.ptr.example", NO_RECOVERY, true },
+
+ /* Test for "-" which may be valid depending on position. */
+ { "Invalid leading \"-\"",
+ "-test.ptr.example", NO_RECOVERY, true },
+ { "Valid trailing \"-\"",
+ "test-.ptr.example", 0, false },
+ { "Valid mid-label use of \"-\"",
+ "te-st.ptr.example", 0, false },
+
+ /* Test for "_" which is always valid in any position. */
+ { "Valid leading use of \"_\"",
+ "_test.ptr.example", 0, false },
+ { "Valid mid-label use of \"_\"",
+ "te_st.ptr.example", 0, false },
+ { "Valid trailing use of \"_\"",
+ "test_.ptr.example", 0, false },
+
+ /* Sanity test the broader set [A-Za-z0-9_-] of valid characters. */
+ { "Valid \"[A-Z]\"",
+ "test.ABCDEFGHIJKLMNOPQRSTUVWXYZ.ptr.example", 0, false },
+ { "Valid \"[a-z]\"",
+ "test.abcdefghijklmnopqrstuvwxyz.ptr.example", 0, false },
+ { "Valid \"[0-9]\"",
+ "test.0123456789.ptr.example", 0, false },
+ { "Valid mixed use of \"[A-Za-z0-9_-]\"",
+ "test.012abcABZ_-.ptr.example", 0, false },
+ };
+
+static void
+response (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ TEST_COMPARE (qclass, C_IN);
+
+ /* We only test PTR. */
+ TEST_COMPARE (qtype, T_PTR);
+
+ unsigned int count, count1;
+ char *tail = NULL;
+
+ /* The test implementation can handle up to 255 tests. */
+ if (strstr (qname, "in-addr.arpa") != NULL
+ && sscanf (qname, "%u.%ms", &count, &tail) == 2)
+ TEST_COMPARE_STRING (tail, "0.168.192.in-addr.arpa");
+ else if (sscanf (qname, "%x.%x.%ms", &count, &count1, &tail) == 3)
+ {
+ TEST_COMPARE_STRING (tail, "\
+0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa");
+ count |= count1 << 4;
+ }
+ else
+ FAIL_EXIT1 ("invalid QNAME: %s\n", qname);
+ free (tail);
+
+ /* Cross check. Count has a fixed bound (soft limit). */
+ TEST_VERIFY (count >= 0 && count <= 255);
+
+ /* We have a fixed number of tests (hard limit). */
+ TEST_VERIFY_EXIT (count < array_length (test_items));
+
+ struct resolv_response_flags flags = {};
+ resolv_response_init (b, flags);
+ resolv_response_add_question (b, qname, qclass, qtype);
+ resolv_response_section (b, ns_s_an);
+
+ /* Actual answer record. */
+ resolv_response_open_record (b, qname, qclass, qtype, 60);
+
+ /* Record the answer. */
+ resolv_response_add_name (b, test_items[count].answer);
+ resolv_response_close_record (b);
+}
+
+/* Perform one check using a reverse lookup. */
+static void
+check_reverse (int af, int count)
+{
+ TEST_VERIFY (af == AF_INET || af == AF_INET6);
+ TEST_VERIFY_EXIT (count < array_length (test_items));
+
+ /* Generate an address to query for each test. */
+ char addr[sizeof (struct in6_addr)] = { 0 };
+ socklen_t addrlen;
+ if (af == AF_INET)
+ {
+ addr[0] = (char) 192;
+ addr[1] = (char) 168;
+ addr[2] = (char) 0;
+ addr[3] = (char) count;
+ addrlen = 4;
+ }
+ else
+ {
+ addr[0] = 0x20;
+ addr[1] = 0x01;
+ addr[2] = 0x0d;
+ addr[3] = 0xb8;
+ addr[4] = addr[5] = addr[6] = addr[7] = 0x0;
+ addr[8] = addr[9] = addr[10] = addr[11] = 0x0;
+ addr[12] = 0x0;
+ addr[13] = 0x0;
+ addr[14] = 0x0;
+ addr[15] = (char) count;
+ addrlen = 16;
+ }
+
+ h_errno = 0;
+ struct hostent *answer = gethostbyaddr (addr, addrlen, af);
+
+ /* Verify h_errno is as expected. */
+ TEST_COMPARE (h_errno, test_items[count].expected);
+ if (h_errno != test_items[count].expected)
+ /* And print more information if it's not. */
+ printf ("INFO: %s\n", test_items[count].test);
+
+ if (test_items[count].fail)
+ {
+ /* We expected a failure so verify answer is NULL. */
+ TEST_VERIFY (answer == NULL);
+ /* If it's not NULL we should print out what we received. */
+ if (answer != NULL)
+ printf ("error: unexpected success: %s\n",
+ support_format_hostent (answer));
+ }
+ else
+ /* We don't expect a failure so answer must be valid. */
+ TEST_COMPARE_STRING (answer->h_name, test_items[count].answer);
+}
+
+static int
+do_test (void)
+{
+ struct resolv_test *obj = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response
+ });
+
+ for (int i = 0; i < array_length (test_items); i++)
+ {
+ check_reverse (AF_INET, i);
+ check_reverse (AF_INET6, i);
+ }
+ resolv_test_end (obj);
+
+ return 0;
+}
+
+#include <support/test-driver.c>