[PATCH] resolv: Add test for gethostbyname_r unaligned buffer [BZ #18287]
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Wed Jun 17 20:17:46 GMT 2026
On 16/06/26 16:17, Sergey Kolosov wrote:
> 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.
> ---
> This patch adds a test for the gethostbyname_r buffer overflow (BZ #18287).
Thanks for working on this, some comments below.
> ---
> resolv/Makefile | 3 +
> resolv/tst-resolv-gethostbyname_r-unaligned.c | 158 ++++++++++++++++++
> 2 files changed, 161 insertions(+)
> create mode 100644 resolv/tst-resolv-gethostbyname_r-unaligned.c
>
> diff --git a/resolv/Makefile b/resolv/Makefile
> index 68b3a4dbf3..ecd4dcbfa0 100644
> --- a/resolv/Makefile
> +++ b/resolv/Makefile
> @@ -157,6 +157,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.
> @@ -306,6 +307,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..7298c6f8e6
> --- /dev/null
> +++ b/resolv/tst-resolv-gethostbyname_r-unaligned.c
> @@ -0,0 +1,158 @@
> +/* 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 <stdlib.h>
> +#include <support/check.h>
> +#include <support/resolv_test.h>
> +#include <support/support.h>
> +#include <string.h>
> +#include <netdb.h>
> +#include <stdio.h>
> +
> +/* 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, "foo.site.example") == 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);
> + }
> + }
> +}
> +
> +static int
> +query_host (const char *host_name, size_t size, int align)
> +{
> + struct hostent ret;
> + struct hostent *result;
> + int err;
> +
> + /* Allocate memory for buffer, align and 64 bytes. */
> + size_t total_alloc = size + align + 64;
> + unsigned char *raw_buf = malloc (total_alloc);
> + TEST_VERIFY_EXIT (raw_buf != NULL);
> +
> + /* Fill in buffer with 0xAA to detect buffer overflow. */
> + memset (raw_buf, 0, size + align);
> + memset (raw_buf + size + align, 0xAA, 64);
Use support_next_to_fault_allocate (support/next_to_fault.h) instead of the
hand-rolled 0xAA guard as localedata/tst-bz12701-lc.c, resolv/tst-inet_pton.c,
and posix/wordexp-test.c. Something like:
struct support_next_to_fault nf =
support_next_to_fault_allocate (size + align);
memset (nf.buffer, 0, size + align);
char *ptr = nf.buffer + align; /* ptr + size lands exactly on the fault page */
int res = gethostbyname_r (host_name, &ret, ptr, size, &result, &err);
[...]
support_next_to_fault_free (&nf);
On a vulnerable glibc it produces a clean SIGSEGV instead of relying on the
corruption landing within 64 bytes.
> +
> + 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. */
> + int overflow_detected = 0;
> + for (int i = 0; i < 64; i++)
> + {
> + if (raw_buf[size + align + i] != 0xAA)
> + {
> + overflow_detected = 1;
> + break;
> + }
> + }
> +
> + free (raw_buf);
> +
> + if (overflow_detected)
> + {
> + printf ("Buffer overflow was detected! (align=%d, size=%zu)\n",
> + align, size);
> + /* Return 99 to indicate that a buffer overflow was detected. */
> + return 99;
> + }
> +
> + if (res == 0 && result != NULL)
> + return 0; /* Buffer is enough. */
> + else if (res == ERANGE || err == NETDB_INTERNAL)
> + return 1; /* Buffer is too small. */
> + else
> + FAIL_EXIT1 ("gethostbyname_r failed unexpectedly: res=%d, err=%d",
> + res, err);
> +}
> +
> +static int
> +do_test (void)
> +{
> + struct resolv_test *aux = resolv_test_start
> + ((struct resolv_redirect_config)
> + {
> + .response_callback = response,
> + });
> +
> + const char *host_name = "foo.site.example";
> + int lower_bound = 512;
> + int upper_bound = 2048;
> +
> + TEST_VERIFY (query_host (host_name, lower_bound, 0) == 1);
> + TEST_VERIFY (query_host (host_name, upper_bound, 0) == 0);
> +
> + printf ("info: Finding smallest buffer size that captures DNS response\n");
> +
> + while (upper_bound != lower_bound + 1)
> + {
> + int size = (lower_bound + upper_bound) / 2;
> + if (query_host (host_name, size, 0) == 0)
> + upper_bound = size;
> + else
> + lower_bound = size;
> + }
> +
> + printf ("info: Boundary found. lower_bound=%d, upper_bound=%d\n",
> + lower_bound, upper_bound);
> +
> + TEST_VERIFY (query_host (host_name, lower_bound, 0) == 1);
> + TEST_VERIFY (query_host (host_name, upper_bound, 0) == 0);
> +
> + /* Trigger the vulnerability.
> + Test all misalignments (1-7 bytes). A patched glibc safely
> + returns 0 (success) or 1 (ERANGE) depending on padding.
> + A vulnerable glibc will fail to account for alignment padding,
> + overflow the buffer, corrupt the 0xAA canary, and return 99. */
> + for (int align = 1; align < 8; align++)
> + {
> + int qres = query_host (host_name, upper_bound, align);
> +
> + if (qres == 99)
> + FAIL_EXIT1 ("Buffer overflow detected at align %d\n", align);
> +
> + /* Return value other than 99 means no overflow occurred. */
> + TEST_VERIFY (qres == 0 || qres == 1);
> + }
> +
> + resolv_test_end (aux);
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
More information about the Libc-alpha
mailing list