* [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437)
@ 2026-03-20 21:51 Carlos O'Donell
2026-03-20 21:52 ` [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438) Carlos O'Donell
2026-03-20 23:26 ` [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Collin Funk
0 siblings, 2 replies; 5+ messages in thread
From: Carlos O'Donell @ 2026-03-20 21:51 UTC (permalink / raw)
To: libc-alpha, fweimer, collin.funk1; +Cc: Carlos O'Donell
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.
---
v1 -> v2
- Split out changes for CVE-2026-4437
- Incorporate Florian's suggestions.
- Remove not-needed xmemstream.h included.
- Fix IPv6 test to properly scan count via sscanf.
- A/B tested changes again to review error messages on failure.
resolv/Makefile | 4 +
resolv/nss_dns/dns-host.c | 2 +-
resolv/tst-resolv-dns-section.c | 162 ++++++++++++++++++++++++++++++++
3 files changed, 167 insertions(+), 1 deletion(-)
create mode 100644 resolv/tst-resolv-dns-section.c
diff --git a/resolv/Makefile b/resolv/Makefile
index 34916a90cc..95bad5df27 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -114,6 +114,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 \
@@ -125,6 +126,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.
@@ -299,6 +301,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 6a60c87532..893137027e 100644
--- a/resolv/nss_dns/dns-host.c
+++ b/resolv/nss_dns/dns-host.c
@@ -820,7 +820,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 0000000000..1171baef51
--- /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>
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438)
2026-03-20 21:51 [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Carlos O'Donell
@ 2026-03-20 21:52 ` Carlos O'Donell
2026-03-25 14:32 ` Adhemerval Zanella Netto
2026-03-20 23:26 ` [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Collin Funk
1 sibling, 1 reply; 5+ messages in thread
From: Carlos O'Donell @ 2026-03-20 21:52 UTC (permalink / raw)
To: libc-alpha, fweimer, collin.funk1; +Cc: Carlos O'Donell
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.
---
v1 -> v2
- Split out changes for CVE-2026-4438
- Incorporate Florian's suggestions.
- Remove not-needed xmemstream.h included.
- Drop superflous printf in test loop (too verbose).
- A/B tested changes again to review error messages on failure.
resolv/Makefile | 3 +
resolv/nss_dns/dns-host.c | 2 +-
resolv/tst-resolv-invalid-ptr.c | 254 ++++++++++++++++++++++++++++++++
3 files changed, 258 insertions(+), 1 deletion(-)
create mode 100644 resolv/tst-resolv-invalid-ptr.c
diff --git a/resolv/Makefile b/resolv/Makefile
index 95bad5df27..971608eff5 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -117,6 +117,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 \
@@ -312,6 +313,8 @@ $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
$(shared-thread-library)
$(objpfx)tst-resolv-invalid-cname: $(objpfx)libresolv.so \
$(shared-thread-library)
+$(objpfx)tst-resolv-invalid-ptr: $(objpfx)libresolv.so \
+ $(shared-thread-library)
$(objpfx)tst-resolv-no-search: $(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)
diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
index 893137027e..728dae615d 100644
--- a/resolv/nss_dns/dns-host.c
+++ b/resolv/nss_dns/dns-host.c
@@ -866,7 +866,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 0000000000..4671935795
--- /dev/null
+++ b/resolv/tst-resolv-invalid-ptr.c
@@ -0,0 +1,254 @@
+/* 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 },
+
+ { NULL, 0, 0 },
+ };
+
+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;
+
+ 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);
+
+ /* We have a bounded number of possible tests. */
+ TEST_VERIFY (count >= 0);
+ TEST_VERIFY (count <= 255);
+
+ 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 (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; 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>
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437)
2026-03-20 21:51 [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Carlos O'Donell
2026-03-20 21:52 ` [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438) Carlos O'Donell
@ 2026-03-20 23:26 ` Collin Funk
2026-03-23 20:58 ` Carlos O'Donell
1 sibling, 1 reply; 5+ messages in thread
From: Collin Funk @ 2026-03-20 23:26 UTC (permalink / raw)
To: Carlos O'Donell; +Cc: libc-alpha, fweimer
Carlos O'Donell <carlos@redhat.com> writes:
> 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.
> ---
> v1 -> v2
> - Split out changes for CVE-2026-4437
> - Incorporate Florian's suggestions.
> - Remove not-needed xmemstream.h included.
> - Fix IPv6 test to properly scan count via sscanf.
> - A/B tested changes again to review error messages on failure.
>
> resolv/Makefile | 4 +
> resolv/nss_dns/dns-host.c | 2 +-
> resolv/tst-resolv-dns-section.c | 162 ++++++++++++++++++++++++++++++++
> 3 files changed, 167 insertions(+), 1 deletion(-)
> create mode 100644 resolv/tst-resolv-dns-section.c
>
> diff --git a/resolv/Makefile b/resolv/Makefile
> index 34916a90cc..95bad5df27 100644
> --- a/resolv/Makefile
> +++ b/resolv/Makefile
> @@ -114,6 +114,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 \
> @@ -125,6 +126,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.
> @@ -299,6 +301,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 6a60c87532..893137027e 100644
> --- a/resolv/nss_dns/dns-host.c
> +++ b/resolv/nss_dns/dns-host.c
> @@ -820,7 +820,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)
> {
This change looks good to me. The rest of the code in that file checks
the answer section boundary in the same way as this change. Also, I
confirmed your test catches the bug.
Reviewed-by: Collin Funk <collin.funk1@gmail.com>
Thanks,
Collin
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437)
2026-03-20 23:26 ` [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Collin Funk
@ 2026-03-23 20:58 ` Carlos O'Donell
0 siblings, 0 replies; 5+ messages in thread
From: Carlos O'Donell @ 2026-03-23 20:58 UTC (permalink / raw)
To: Collin Funk; +Cc: libc-alpha, fweimer
On 3/20/26 7:26 PM, Collin Funk wrote:
> Carlos O'Donell <carlos@redhat.com> writes:
>
>> 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.
>> ---
>> v1 -> v2
>> - Split out changes for CVE-2026-4437
>> - Incorporate Florian's suggestions.
>> - Remove not-needed xmemstream.h included.
>> - Fix IPv6 test to properly scan count via sscanf.
>> - A/B tested changes again to review error messages on failure.
>>
>> resolv/Makefile | 4 +
>> resolv/nss_dns/dns-host.c | 2 +-
>> resolv/tst-resolv-dns-section.c | 162 ++++++++++++++++++++++++++++++++
>> 3 files changed, 167 insertions(+), 1 deletion(-)
>> create mode 100644 resolv/tst-resolv-dns-section.c
>>
>> diff --git a/resolv/Makefile b/resolv/Makefile
>> index 34916a90cc..95bad5df27 100644
>> --- a/resolv/Makefile
>> +++ b/resolv/Makefile
>> @@ -114,6 +114,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 \
>> @@ -125,6 +126,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.
>> @@ -299,6 +301,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 6a60c87532..893137027e 100644
>> --- a/resolv/nss_dns/dns-host.c
>> +++ b/resolv/nss_dns/dns-host.c
>> @@ -820,7 +820,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)
>> {
>
> This change looks good to me. The rest of the code in that file checks
> the answer section boundary in the same way as this change. Also, I
> confirmed your test catches the bug.
>
> Reviewed-by: Collin Funk <collin.funk1@gmail.com>
Thanks. Pushed. I'll update the advisory with the fixed commit.
--
Cheers,
Carlos.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438)
2026-03-20 21:52 ` [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438) Carlos O'Donell
@ 2026-03-25 14:32 ` Adhemerval Zanella Netto
0 siblings, 0 replies; 5+ messages in thread
From: Adhemerval Zanella Netto @ 2026-03-25 14:32 UTC (permalink / raw)
To: Carlos O'Donell, libc-alpha, fweimer, collin.funk1
On 20/03/26 18:52, Carlos O'Donell wrote:
> 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.
LGTM, thanks. Just minor suggestions below.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
> ---
> v1 -> v2
> - Split out changes for CVE-2026-4438
> - Incorporate Florian's suggestions.
> - Remove not-needed xmemstream.h included.
> - Drop superflous printf in test loop (too verbose).
> - A/B tested changes again to review error messages on failure.
>
>
> resolv/Makefile | 3 +
> resolv/nss_dns/dns-host.c | 2 +-
> resolv/tst-resolv-invalid-ptr.c | 254 ++++++++++++++++++++++++++++++++
> 3 files changed, 258 insertions(+), 1 deletion(-)
> create mode 100644 resolv/tst-resolv-invalid-ptr.c
>
> diff --git a/resolv/Makefile b/resolv/Makefile
> index 95bad5df27..971608eff5 100644
> --- a/resolv/Makefile
> +++ b/resolv/Makefile
> @@ -117,6 +117,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 \
> @@ -312,6 +313,8 @@ $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
> $(shared-thread-library)
> $(objpfx)tst-resolv-invalid-cname: $(objpfx)libresolv.so \
> $(shared-thread-library)
> +$(objpfx)tst-resolv-invalid-ptr: $(objpfx)libresolv.so \
> + $(shared-thread-library)
> $(objpfx)tst-resolv-no-search: $(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)
> diff --git a/resolv/nss_dns/dns-host.c b/resolv/nss_dns/dns-host.c
> index 893137027e..728dae615d 100644
> --- a/resolv/nss_dns/dns-host.c
> +++ b/resolv/nss_dns/dns-host.c
> @@ -866,7 +866,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;
Ok.
> diff --git a/resolv/tst-resolv-invalid-ptr.c b/resolv/tst-resolv-invalid-ptr.c
> new file mode 100644
> index 0000000000..4671935795
> --- /dev/null
> +++ b/resolv/tst-resolv-invalid-ptr.c
> @@ -0,0 +1,254 @@
> +/* 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 },
> +
> + { NULL, 0, 0 },
You don't really this sentinel value.
> + };
> +
> +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;
> +
> + 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);
> +
> + /* We have a bounded number of possible tests. */
> + TEST_VERIFY (count >= 0);
> + TEST_VERIFY (count <= 255);
Maybe use 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 (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; test_items[i].test != NULL ; i++)
Maybe use
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>
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-03-25 14:32 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-20 21:51 [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Carlos O'Donell
2026-03-20 21:52 ` [PATCH v2 2/2] resolv: Check hostname for validity (CVE-2026-4438) Carlos O'Donell
2026-03-25 14:32 ` Adhemerval Zanella Netto
2026-03-20 23:26 ` [PATCH v2 1/2] resolv: Count records correctly (CVE-2026-4437) Collin Funk
2026-03-23 20:58 ` Carlos O'Donell
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).