[glibc] resolv: Add test for gethostbyname_r unaligned buffer [BZ #18287]

Florian Weimer via Glibc-cvs <[email protected]> Tue, 23 Jun 2026 07:58:58 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=b61b98e3d0c81c85200e2082407708c7b0e821a3

commit b61b98e3d0c81c85200e2082407708c7b0e821a3
Author: Sergey Kolosov <[email protected]>
Date:   Mon Jun 22 20:15:44 2026 +0200

    resolv: Add test for gethostbyname_r unaligned buffer [BZ #18287]
    
    Add a test for the buffer overflow in gethostbyname_r (Bug 18287), which
    occurs when alignment padding is not subtracted from the remaining buffer
    length.
    
    The test uses binary search to find the exact required buffer size
    for a mocked DNS response. It then tests all pointer misalignments
    (1-7 bytes) using a custom 0xAA guard region. This guarantees
    deterministic detection of out-of-bounds writes, avoiding false
    negatives caused by malloc chunk rounding.
    
    Reviewed-by: Florian Weimer <[email protected]>

Diff:
---
 resolv/Makefile                               |   3 +
 resolv/tst-resolv-gethostbyname_r-unaligned.c | 161 ++++++++++++++++++++++++++
 2 files changed, 164 insertions(+)

diff --git a/resolv/Makefile b/resolv/Makefile
index 02cc751732..28f6ba6c3b 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -158,6 +158,7 @@ tests += \
   tst-resolv-ai_idn-nolibidn2 \
   tst-resolv-canonname \
   tst-resolv-getaddrinfo-fqdn \
+  tst-resolv-gethostbyname_r-unaligned \
   tst-resolv-trustad \
 
 # Needs resolv_context.
@@ -307,6 +308,8 @@ $(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-gethostbyname_r-unaligned: \
+  $(objpfx)libresolv.so $(shared-thread-library)
 $(objpfx)tst-resolv-network: $(objpfx)libresolv.so $(shared-thread-library)
 $(objpfx)tst-resolv-res_init: $(objpfx)libresolv.so
 $(objpfx)tst-resolv-res_init-failure: $(objpfx)libresolv.so \
diff --git a/resolv/tst-resolv-gethostbyname_r-unaligned.c b/resolv/tst-resolv-gethostbyname_r-unaligned.c
new file mode 100644
index 0000000000..dc1535c8db
--- /dev/null
+++ b/resolv/tst-resolv-gethostbyname_r-unaligned.c
@@ -0,0 +1,161 @@
+/* Test for BZ #18287.
+   This test verifies that gethostbyname_r correctly accounts for pointer
+   alignment padding when calculating the remaining buffer size.
+   Copyright (C) 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 <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/check_nss.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+#include <support/xmemstream.h>
+
+static const char host_name[] = "foo.site.example";
+
+/* Prepare big enough answer to trigger buffer overflow.  */
+static void
+response (const struct resolv_response_context *ctx,
+          struct resolv_response_builder *b,
+          const char *qname, uint16_t qclass, uint16_t qtype)
+{
+  if (strcmp (qname, host_name) == 0 && qtype == T_A)
+    {
+      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);
+
+      for (int i = 0; i <= 60; i++)
+        {
+          char last_ch = (char) i;
+          char addr_ipv4[4] = { 127, 126, 125, last_ch };
+          resolv_response_open_record (b, qname, qclass, T_A, 0x12345678);
+          resolv_response_add_data (b, addr_ipv4, sizeof (addr_ipv4));
+          resolv_response_close_record (b);
+        }
+    }
+}
+
+/* Test gethostbyname_r with a specified buffer size and alignment.
+   Returns true if the buffer is sufficient, false if it's too small.  */
+static bool
+query_host (size_t size, size_t align)
+{
+  struct hostent ret;
+  struct hostent *result = NULL;
+  int err;
+
+  /* Allocate memory for buffer, alignment padding, and a 64-byte checking
+     area.  */
+  size_t total_alloc = size + align + 64;
+  unsigned char *raw_buf = xmalloc (total_alloc);
+
+  /* Fill the tail of the buffer with 0xAA to detect overflows below.  */
+  memset (raw_buf, 0, size + align);
+  memset (raw_buf + size + align, 0xAA, 64);
+
+  char *ptr = (char *) raw_buf + align;
+
+  int res = gethostbyname_r (host_name, &ret, ptr, size, &result, &err);
+
+  /* Verify that the overflow guard region remains unchanged.  */
+  for (int i = 0; i < 64; i++)
+    if (raw_buf[size + align + i] != 0xAA)
+      FAIL_EXIT1 ("Buffer overflow was detected! (align=%zu, size=%zu)",
+                  align, size);
+
+  bool is_sufficient = false;
+
+  if (res == 0 && result != NULL)
+    {
+      /* Generate the expected response to satisfy check_hostent.  */
+      struct xmemstream expected;
+      xopen_memstream (&expected);
+
+      fprintf (expected.out, "name: %s\n", host_name);
+      for (int i = 0; i <= 60; i++)
+        fprintf (expected.out, "address: 127.126.125.%d\n", i);
+
+      xfclose_memstream (&expected);
+      check_hostent (host_name, &ret, expected.buffer);
+      free (expected.buffer);
+      is_sufficient = true; /* Buffer is sufficient.  */
+    }
+  else if (res == ERANGE && err == NETDB_INTERNAL)
+    is_sufficient = false; /* Buffer is too small.  */
+  else
+    FAIL_EXIT1 ("gethostbyname_r failed unexpectedly: res=%d, err=%d",
+                res, err);
+
+  free (raw_buf);
+  return is_sufficient;
+}
+
+static int
+do_test (void)
+{
+  struct resolv_test *aux = resolv_test_start
+    ((struct resolv_redirect_config)
+     {
+       .response_callback = response,
+     });
+
+  int lower_bound = 512;
+  int upper_bound = 2048;
+
+  TEST_COMPARE (query_host (lower_bound, 0), false);
+  TEST_COMPARE (query_host (upper_bound, 0), true);
+
+  /* Finding the smallest hostent buffer size.  */
+  while (upper_bound != lower_bound + 1)
+    {
+      int size = (lower_bound + upper_bound) / 2;
+      if (query_host (size, 0))
+        upper_bound = size;
+      else
+        lower_bound = size;
+    }
+
+  printf ("info: Boundary found. lower_bound=%d, upper_bound=%d\n",
+          lower_bound, upper_bound);
+
+  TEST_COMPARE (query_host (lower_bound, 0), false);
+  TEST_COMPARE (query_host (upper_bound, 0), true);
+
+  /* Trigger the vulnerability.
+     Test all misalignments (1-7 bytes).  Note that it is expected that
+     for certain alignments, the required buffer size increases.  This
+     happens because gethostbyname_r applies internal padding to align
+     pointers, which consumes available space.  Therefore, a patched glibc
+     safely returns true (success) or false (ERANGE) depending on this
+     padding.  A vulnerable glibc will fail to account for alignment
+     padding, overflow the buffer, and cause a test failure.  */
+  for (size_t align = 1; align < 8; align++)
+    query_host (upper_bound, align);
+
+  resolv_test_end (aux);
+
+  return 0;
+}
+
+#include <support/test-driver.c>