[glibc/release/2.36/master] resolv: Check hostname for validity (CVE-2026-4438)

Aurelien Jarno via Glibc-cvs <[email protected]>
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=d549918c84928e2fb18a756d2c0e40da097da3fe

commit d549918c84928e2fb18a756d2c0e40da097da3fe
Author: Carlos O'Donell <[email protected]>
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  <[email protected]>
    (cherry picked from commit e10977481f4db4b2a3ce34fa4c3a1e26651ae312)

Diff:
---
 resolv/Makefile                 |   3 +
 resolv/nss_dns/dns-host.c       |   2 +-
 resolv/tst-resolv-invalid-ptr.c | 255 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 259 insertions(+), 1 deletion(-)

diff --git a/resolv/Makefile b/resolv/Makefile
index a13f75d858..d1b22f5b64 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -100,6 +100,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 \
@@ -297,6 +298,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-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 75368f0fea..7053f212ae 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..0c802ab967
--- /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 \"@\"",
+      "[email protected]", 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>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.