resolv: Count records, and check hostname (CVE-2026-4437, CVE-2026-4438)

Message ID 20260320194250.1089143-1-carlos@redhat.com (mailing list archive)
State Failed CI
Headers
Series resolv: Count records, and check hostname (CVE-2026-4437, CVE-2026-4438) |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-arm success Test passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed

Commit Message

Carlos O'Donell March 20, 2026, 7:42 p.m. UTC
  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.

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.

These two issues are considered distinct CVEs, but are fixed in one
commit to make the update process easier, given that they change the
same file and function.

Regression tests are added for invalid metacharacters and response
section crossing.

No regressions on x86_64-linux-gnu.
---
 resolv/Makefile                 |   7 +
 resolv/nss_dns/dns-host.c       |   4 +-
 resolv/tst-resolv-dns-section.c | 161 ++++++++++++++++++++
 resolv/tst-resolv-invalid-ptr.c | 258 ++++++++++++++++++++++++++++++++
 4 files changed, 428 insertions(+), 2 deletions(-)
 create mode 100644 resolv/tst-resolv-dns-section.c
 create mode 100644 resolv/tst-resolv-invalid-ptr.c
  

Comments

Florian Weimer March 20, 2026, 8:11 p.m. UTC | #1
* 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.
>
> 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.
>
> These two issues are considered distinct CVEs, but are fixed in one
> commit to make the update process easier, given that they change the
> same file and function.
>
> Regression tests are added for invalid metacharacters and response
> section crossing.
>
> No regressions on x86_64-linux-gnu.
> ---
>  resolv/Makefile                 |   7 +
>  resolv/nss_dns/dns-host.c       |   4 +-
>  resolv/tst-resolv-dns-section.c | 161 ++++++++++++++++++++
>  resolv/tst-resolv-invalid-ptr.c | 258 ++++++++++++++++++++++++++++++++
>  4 files changed, 428 insertions(+), 2 deletions(-)
>  create mode 100644 resolv/tst-resolv-dns-section.c
>  create mode 100644 resolv/tst-resolv-invalid-ptr.c

I don't quite understand why this is a single commit for two bugs that
have very different impact, fixes, and even separate test cases.

Why not split this into two?  That makes the accompanying advisory text
clearer, too.

Thanks,
Florian
  
Carlos O'Donell March 20, 2026, 8:20 p.m. UTC | #2
On 3/20/26 4:11 PM, Florian Weimer wrote:
> * 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.
>>
>> 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.
>>
>> These two issues are considered distinct CVEs, but are fixed in one
>> commit to make the update process easier, given that they change the
>> same file and function.
>>
>> Regression tests are added for invalid metacharacters and response
>> section crossing.
>>
>> No regressions on x86_64-linux-gnu.
>> ---
>>   resolv/Makefile                 |   7 +
>>   resolv/nss_dns/dns-host.c       |   4 +-
>>   resolv/tst-resolv-dns-section.c | 161 ++++++++++++++++++++
>>   resolv/tst-resolv-invalid-ptr.c | 258 ++++++++++++++++++++++++++++++++
>>   4 files changed, 428 insertions(+), 2 deletions(-)
>>   create mode 100644 resolv/tst-resolv-dns-section.c
>>   create mode 100644 resolv/tst-resolv-invalid-ptr.c
> 
> I don't quite understand why this is a single commit for two bugs that
> have very different impact, fixes, and even separate test cases.

The justification is in the commit log? :-)
~~~
These two issues are considered distinct CVEs, but are fixed in one
commit to make the update process easier, given that they change the
same file and function.
~~~
  
> Why not split this into two?  That makes the accompanying advisory text
> clearer, too.

Would you still like them split up?

I don't have a strong opinion.
  
Collin Funk March 20, 2026, 8:27 p.m. UTC | #3
Carlos O'Donell <carlos@redhat.com> writes:

>
> The justification is in the commit log? :-)
> ~~~
> These two issues are considered distinct CVEs, but are fixed in one
> commit to make the update process easier, given that they change the
> same file and function.
> ~~~
>  > Why not split this into two?  That makes the accompanying advisory
>   text
>> clearer, too.
>
> Would you still like them split up?
>
> I don't have a strong opinion.

FWIW, I think separate patches makes things easier to review.

Collin
  
Florian Weimer March 20, 2026, 8:39 p.m. UTC | #4
* Carlos O'Donell:

> On 3/20/26 4:11 PM, Florian Weimer wrote:
>> * 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.
>>>
>>> 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.
>>>
>>> These two issues are considered distinct CVEs, but are fixed in one
>>> commit to make the update process easier, given that they change the
>>> same file and function.
>>>
>>> Regression tests are added for invalid metacharacters and response
>>> section crossing.
>>>
>>> No regressions on x86_64-linux-gnu.
>>> ---
>>>   resolv/Makefile                 |   7 +
>>>   resolv/nss_dns/dns-host.c       |   4 +-
>>>   resolv/tst-resolv-dns-section.c | 161 ++++++++++++++++++++
>>>   resolv/tst-resolv-invalid-ptr.c | 258 ++++++++++++++++++++++++++++++++
>>>   4 files changed, 428 insertions(+), 2 deletions(-)
>>>   create mode 100644 resolv/tst-resolv-dns-section.c
>>>   create mode 100644 resolv/tst-resolv-invalid-ptr.c

>> I don't quite understand why this is a single commit for two bugs
>> that have very different impact, fixes, and even separate test cases.
>
> The justification is in the commit log? :-)
> ~~~
> These two issues are considered distinct CVEs, but are fixed in one
> commit to make the update process easier, given that they change the
> same file and function.
> ~~~

If that's the only justification, I don't think we should put both into
one patch.

Couple of review notes:

> +  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) == 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.0.8.b.d.0.1.0.0.2.ip6.arpa");
> +    }

It seems that tail is never freed?

This also applies to the second test case.

> +  TEST_VERIFY (count <= 15);

> +  TEST_VERIFY (count < 16);

These checks should use array_length (test_items).

This also applies to the second test case.

> +  if (answer != NULL)
> +    {
> +      printf ("INFO: %s\n", support_format_hostent (answer));
> +      TEST_COMPARE_STRING (answer->h_name, "test.ptr.example.net");
> +    }

This is after the failure, and the information is already included in
the support_format_hostent output, so the TEST_COMPARE_STRING seems
quite redundant?  And printf could say "error: unexpected success: %s\n".
The last part also applies to the second test.

> +    /* 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 },

The use of UTF-8 might be tricky to backport for some environments.

> +      TEST_VERIFY (answer != NULL);
> +      /* And it must match what we provided.  */
> +      if (answer != NULL)
> +        TEST_COMPARE_STRING (answer->h_name, test_items[count].answer);

TEST_COMPARE_STRING already handles NULL, so the extra checks aren't
necessary.

Thanks,
Florian
  
Carlos O'Donell March 20, 2026, 9:28 p.m. UTC | #5
On 3/20/26 4:39 PM, Florian Weimer wrote:
> * Carlos O'Donell:
> 
>> On 3/20/26 4:11 PM, Florian Weimer wrote:
>>> * 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.
>>>>
>>>> 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.
>>>>
>>>> These two issues are considered distinct CVEs, but are fixed in one
>>>> commit to make the update process easier, given that they change the
>>>> same file and function.
>>>>
>>>> Regression tests are added for invalid metacharacters and response
>>>> section crossing.
>>>>
>>>> No regressions on x86_64-linux-gnu.
>>>> ---
>>>>    resolv/Makefile                 |   7 +
>>>>    resolv/nss_dns/dns-host.c       |   4 +-
>>>>    resolv/tst-resolv-dns-section.c | 161 ++++++++++++++++++++
>>>>    resolv/tst-resolv-invalid-ptr.c | 258 ++++++++++++++++++++++++++++++++
>>>>    4 files changed, 428 insertions(+), 2 deletions(-)
>>>>    create mode 100644 resolv/tst-resolv-dns-section.c
>>>>    create mode 100644 resolv/tst-resolv-invalid-ptr.c
> 
>>> I don't quite understand why this is a single commit for two bugs
>>> that have very different impact, fixes, and even separate test cases.
>>
>> The justification is in the commit log? :-)
>> ~~~
>> These two issues are considered distinct CVEs, but are fixed in one
>> commit to make the update process easier, given that they change the
>> same file and function.
>> ~~~
> 
> If that's the only justification, I don't think we should put both into
> one patch.

I've split the the commit in two to cover one CVE per commit.

Please note that after review of the last few CVEs the glibc security team
updated the team process on 2026-01-15 to limit backport work to the most
recent released branch e.g. release/2.43/master.

See 9.2 in https://sourceware.org/glibc/wiki/CNA/Response

We leave it up to the interested developers to do further backports.

With increased security reports arriving the glibc security team wants to
make sure we have capacity to handle new reports.

> Couple of review notes:
> 
>> +  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) == 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.0.8.b.d.0.1.0.0.2.ip6.arpa");
>> +    }
> 
> It seems that tail is never freed?
> 
> This also applies to the second test case.

Correct, both need free (tail) given the use of %m. Fixed in both.

Added a FAIL_EXIT to this test for invalid qname to match similar tests.

>> +  TEST_VERIFY (count <= 15);
> 
>> +  TEST_VERIFY (count < 16);
> 
> These checks should use array_length (test_items).
> 
> This also applies to the second test case.

Agreed. Fixed in both.

>> +  if (answer != NULL)
>> +    {
>> +      printf ("INFO: %s\n", support_format_hostent (answer));
>> +      TEST_COMPARE_STRING (answer->h_name, "test.ptr.example.net");
>> +    }
> 
> This is after the failure, and the information is already included in
> the support_format_hostent output, so the TEST_COMPARE_STRING seems
> quite redundant?  And printf could say "error: unexpected success: %s\n".
> The last part also applies to the second test.

Good point. I've removed the TEST_COMPARE_STRING since it's redundant. Fixed.

I've changed the printf to "error: unexpected success: %s\n" in both tests. Fixed.

>> +    /* 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 },
> 
> The use of UTF-8 might be tricky to backport for some environments.

Windows DNS claims to support UTF-8 characters in names:
https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou

So I wanted to include a test that showed UTF-8 as invalid since our
implementation doesn't currently support it.

My preference is to leave them, and if they are difficult to backport we
can revisit a simplified or alternate test that encodes them in a
different way?

>> +      TEST_VERIFY (answer != NULL);
>> +      /* And it must match what we provided.  */
>> +      if (answer != NULL)
>> +        TEST_COMPARE_STRING (answer->h_name, test_items[count].answer);
> 
> TEST_COMPARE_STRING already handles NULL, so the extra checks aren't
> necessary.

Fixed.

I'll post v2.
  
Collin Funk March 20, 2026, 10:07 p.m. UTC | #6
Carlos O'Donell <carlos@redhat.com> writes:

>>> +    /* 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 },
>> The use of UTF-8 might be tricky to backport for some environments.
>
> Windows DNS claims to support UTF-8 characters in names:
> https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou
>
> So I wanted to include a test that showed UTF-8 as invalid since our
> implementation doesn't currently support it.
>
> My preference is to leave them, and if they are difficult to backport we
> can revisit a simplified or alternate test that encodes them in a
> different way?

It is less readable, but what we do in Gnulib is just write the UTF-8
characters in bytes. For example, "ऄअआ" would be written as
"\xE0\xA4\x84\xE0\xA4\x85\xE0\xA4\x86".

Collin
  
Carlos O'Donell March 20, 2026, 10:21 p.m. UTC | #7
On 3/20/26 6:07 PM, Collin Funk wrote:
> Carlos O'Donell <carlos@redhat.com> writes:
> 
>>>> +    /* 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 },
>>> The use of UTF-8 might be tricky to backport for some environments.
>>
>> Windows DNS claims to support UTF-8 characters in names:
>> https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou
>>
>> So I wanted to include a test that showed UTF-8 as invalid since our
>> implementation doesn't currently support it.
>>
>> My preference is to leave them, and if they are difficult to backport we
>> can revisit a simplified or alternate test that encodes them in a
>> different way?
> 
> It is less readable, but what we do in Gnulib is just write the UTF-8
> characters in bytes. For example, "ऄअआ" would be written as
> "\xE0\xA4\x84\xE0\xA4\x85\xE0\xA4\x86".

And error prone.

I have a strong opinion here, and I think we should do one of these:

(a) Use UTF-8 directly.

(b) Build up support/* infrastructure to specify codepoints
     that can be inserted to build the final string.

Thoughts?
  
Collin Funk March 20, 2026, 11:49 p.m. UTC | #8
Carlos O'Donell <carlos@redhat.com> writes:

> On 3/20/26 6:07 PM, Collin Funk wrote:
>> Carlos O'Donell <carlos@redhat.com> writes:
>> 
>>>>> +    /* 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 },
>>>> The use of UTF-8 might be tricky to backport for some environments.
>>>
>>> Windows DNS claims to support UTF-8 characters in names:
>>> https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/naming-conventions-for-computer-domain-site-ou
>>>
>>> So I wanted to include a test that showed UTF-8 as invalid since our
>>> implementation doesn't currently support it.
>>>
>>> My preference is to leave them, and if they are difficult to backport we
>>> can revisit a simplified or alternate test that encodes them in a
>>> different way?
>> It is less readable, but what we do in Gnulib is just write the
>> UTF-8
>> characters in bytes. For example, "ऄअआ" would be written as
>> "\xE0\xA4\x84\xE0\xA4\x85\xE0\xA4\x86".
>
> And error prone.

I agree. I guess with Gnulib we have more portability concerns that
glibc does not need to consider.

> I have a strong opinion here, and I think we should do one of these:
>
> (a) Use UTF-8 directly.
>
> (b) Build up support/* infrastructure to specify codepoints
>     that can be inserted to build the final string.
>
> Thoughts?

How about introducing UTF-8 directly just in this one place for now?
Then give it some time to see if it causes anyone trouble before
introducing more in the future.

Collin
  

Patch

diff --git a/resolv/Makefile b/resolv/Makefile
index 34916a90cc..971608eff5 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -114,8 +114,10 @@  tests += \
   tst-resolv-basic \
   tst-resolv-binary \
   tst-resolv-byaddr \
+  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 \
@@ -125,6 +127,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 +302,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
@@ -308,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 6a60c87532..728dae615d 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))
@@ -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-dns-section.c b/resolv/tst-resolv-dns-section.c
new file mode 100644
index 0000000000..9f55c870a6
--- /dev/null
+++ b/resolv/tst-resolv-dns-section.c
@@ -0,0 +1,161 @@ 
+/* 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 <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>
+#include <support/xmemstream.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) == 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.0.8.b.d.0.1.0.0.2.ip6.arpa");
+    }
+
+  /* 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 < 16);
+
+  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 ("INFO: %s\n", support_format_hostent (answer));
+      TEST_COMPARE_STRING (answer->h_name, "test.ptr.example.net");
+    }
+}
+
+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>
diff --git a/resolv/tst-resolv-invalid-ptr.c b/resolv/tst-resolv-invalid-ptr.c
new file mode 100644
index 0000000000..e07c6b92af
--- /dev/null
+++ b/resolv/tst-resolv-invalid-ptr.c
@@ -0,0 +1,258 @@ 
+/* 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 <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>
+#include <support/xmemstream.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);
+
+  /* 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 < 256);
+
+  /* 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 ("INFO: %s\n", support_format_hostent (answer));
+    }
+  else
+    {
+      /* We don't expect a failure so answer must be valid.  */
+      TEST_VERIFY (answer != NULL);
+      /* And it must match what we provided.  */
+      if (answer != NULL)
+        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++)
+    {
+      printf ("INFO: %s\n", test_items[i].test);
+      check_reverse (AF_INET, i);
+      check_reverse (AF_INET6, i);
+    }
+  resolv_test_end (obj);
+
+  return 0;
+}
+
+#include <support/test-driver.c>