[PATCH v2] resolv: Check hostname for validity (CVE-2026-4438)

Petr Menšík pemensik@redhat.com
Wed Apr 29 17:50:52 GMT 2026


Hi!

I am not sure I read this change correctly. DNS responses in general 
should be binary safe. That means, IDN 2008 is a thing and hostnames 
should not contain only [0-9A-Za-z_-] in their presentation form. They 
can contain also different things. I am not sure how much it needs to be 
handled in PTR responses.

$ dig +idnout xn--pota-h6a.xn--hkyrky-ptac70bc.cz. +noall +answer
pošta.háčkyčárky.cz. 1721    IN    A    217.31.204.67

Unfortunately they do not have reverse address with IDN name also. At 
least with resolv.conf option

options no-check-names

I think it should allow escaped responses, when that option is used.

But! I think in such cases those names *must* be properly escaped. DNS 
escaping is defined in 5.1 section of RFC 1035. It should allow 
hostname\.with\.dot.example.org name. It is a corner case somehow. But 
you can query server by dig 'hostname\.with\.dot.example.org' and it 
should respond with NOERROR or NXDOMAIN. Not SERVFAIL equivalent. Or 
\ddd form, which is decadic number to be more confusing.

I am afraid there is not really anything like acceptable characters in a 
hostname, defined by any recent RFCs. It is better than to allow 
execution of shell commands, but the proper way is enforced escaping 
without refusing responses. Just my 2 cents.

Best Regards,
Petr

On 26/03/2026 13:58, 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.
> ---
> v1 -> v2
> - Use array_length for test iteration.
> - Sanity check count and enforce limit with array_length.
>
>   resolv/Makefile                 |   3 +
>   resolv/nss_dns/dns-host.c       |   2 +-
>   resolv/tst-resolv-invalid-ptr.c | 255 ++++++++++++++++++++++++++++++++
>   3 files changed, 259 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..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 \"@\"",
> +      "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>

-- 
Petr Menšík
Senior Software Engineer, RHEL
Red Hat, https://www.redhat.com/
PGP: DFCF908DB7C87E8E529925BC4931CA5B6C9FC5CB



More information about the Libc-alpha mailing list