Avoid duplicate DNS query if search list contains '.'

Resolves: RHEL-153056
This commit is contained in:
Florian Weimer 2026-03-23 16:53:51 +01:00
parent 6a5da8faa9
commit d58e05d599
3 changed files with 605 additions and 0 deletions

249
glibc-RHEL-153056-1.patch Normal file
View File

@ -0,0 +1,249 @@
commit 8ca2fe7e96c0ccf04d32d7002d7a6d9edcb9f8ee
Author: Sergey Kolosov <skolosov@redhat.com>
Date: Fri Oct 10 17:15:27 2025 +0200
resolv: Add tests for getaddrinfo returning EAI_AGAIN [BZ #16849]
This patch adds two tests that verify correct behavior of getaddrinfo
when DNS resolution fails with a temporary error. Both tests ensure
that getaddrinfo returns EAI_AGAIN in cases where no valid address can
be resolved due to network or resolver failure.
* tst-getaddrinfo-eai-again.c
Runs inside the glibc test-container without any DNS server
configured. The test performs queries using AF_INET, AF_INET6,
and AF_UNSPEC and verifies that getaddrinfo returns EAI_AGAIN
when resolution fails.
* tst-getaddrinfo-eai-again-timeout.c
Runs outside of the container but uses the resolv_test framework
to simulate network failures. The test covers two failure modes:
- No response from the server (resolv_response_drop)
- Zero-length reply from the server
In both cases, getaddrinfo is expected to return EAI_AGAIN.
Reviewed-by: Florian Weimer <fweimer@redhat.com>
diff --git a/resolv/Makefile b/resolv/Makefile
index f3f5c260d0b7471f..cb1ff182dfdd4572 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -80,7 +80,10 @@ routines_no_fortify += \
# routines_no_fortify
tests = tst-aton tst-leaks tst-inet_ntop
-tests-container = tst-leaks2
+tests-container += \
+ tst-getaddrinfo-eai-again \
+ tst-leaks2 \
+ # tests-container
tests-internal += tst-inet_aton_exact
@@ -136,6 +139,7 @@ tests-static += tst-ns_rr_cursor
# These tests need libdl.
ifeq (yes,$(build-shared))
tests += \
+ tst-getaddrinfo-eai-again-timeout \
tst-resolv-ai_idn \
tst-resolv-ai_idn-latin1 \
tst-resolv-ai_idn-nolibidn2 \
@@ -268,6 +272,8 @@ $(objpfx)mtrace-tst-resolv-res_ninit.out: $(objpfx)tst-resolv-res_ninit.out
$(objpfx)tst-bug18665-tcp: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-bug18665: $(objpfx)libresolv.so $(shared-thread-library)
+$(objpfx)tst-getaddrinfo-eai-again-timeout: \
+ $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-ai_idn: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-ai_idn-latin1: \
$(objpfx)libresolv.so $(shared-thread-library)
diff --git a/resolv/tst-getaddrinfo-eai-again-timeout.c b/resolv/tst-getaddrinfo-eai-again-timeout.c
new file mode 100644
index 0000000000000000..ec4a6563b7b5ae51
--- /dev/null
+++ b/resolv/tst-getaddrinfo-eai-again-timeout.c
@@ -0,0 +1,122 @@
+/* Test for BZ #16849. Verify that getaddrinfo correctly returns
+ EAI_AGAIN when DNS resolution fails due to timeout or malformed
+ responses.
+
+ This test uses two simulated failure modes:
+ - The DNS server does not respond at all (resolv_response_drop).
+ - The DNS server responds with a zero-length packet.
+
+ Copyright (C) 2025 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 <netdb.h>
+#include <resolv.h>
+#include <stdbool.h>
+#include <support/check.h>
+#include <support/resolv_test.h>
+
+/* Track whether the callbacks were actually invoked. */
+static volatile bool response_called_drop = false;
+static volatile bool response_called_zero_len = false;
+
+/* Simulate a DNS server that sends a zero-length response. */
+static void
+response_zero_len (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ response_called_zero_len = true;
+ /* Do nothing — zero-length reply. */
+}
+
+/* Simulate a DNS server that drops the request. */
+static void
+response_drop (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ response_called_drop = true;
+ resolv_response_drop (b);
+}
+
+/* Query getaddrinfo for multiple families and expect EAI_AGAIN. */
+static void
+query_host (const char *host_name)
+{
+ int family[] = { AF_INET, AF_INET6, AF_UNSPEC };
+ const char *family_names[] = { "AF_INET", "AF_INET6", "AF_UNSPEC" };
+
+ for (int i = 0; i < 3; i++)
+ {
+ struct addrinfo hints =
+ {
+ .ai_socktype = 0,
+ .ai_protocol = 0,
+ .ai_family = family[i],
+ .ai_flags = 0,
+ };
+ struct addrinfo *result;
+ int res = getaddrinfo (host_name, NULL, &hints, &result);
+ if (res != EAI_AGAIN)
+ FAIL_EXIT1 ("getaddrinfo (%s, %s) returned %s, expected EAI_AGAIN",
+ host_name, family_names[i], gai_strerror (res));
+ }
+}
+
+/* Simulate DNS server dropping all queries. */
+static void
+test_drop (void)
+{
+ struct resolv_test *aux = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response_drop,
+ });
+ /* Reduce default timeout to make the test run faster. */
+ _res.retrans = 1;
+ _res.retry = 1;
+ query_host ("site.example");
+ resolv_test_end (aux);
+}
+
+/* Simulate DNS server sending zero-length responses. */
+static void
+test_zero_len_packet (void)
+{
+ struct resolv_test *aux = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response_zero_len,
+ });
+ query_host ("site.example");
+ resolv_test_end (aux);
+}
+
+static int
+do_test (void)
+{
+ test_drop ();
+ test_zero_len_packet ();
+
+ if (!response_called_drop)
+ FAIL_EXIT1 ("response_drop callback was not called");
+ if (!response_called_zero_len)
+ FAIL_EXIT1 ("response_zero_len callback was not called");
+ return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/resolv/tst-getaddrinfo-eai-again.c b/resolv/tst-getaddrinfo-eai-again.c
new file mode 100644
index 0000000000000000..21daa6c1682d1156
--- /dev/null
+++ b/resolv/tst-getaddrinfo-eai-again.c
@@ -0,0 +1,56 @@
+/* Test for BZ #16849. Verify that getaddrinfo correctly returns
+ EAI_AGAIN error code if DNS query fails due to a network failure.
+
+ Copyright (C) 2025 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 <netdb.h>
+#include <support/check.h>
+
+/* Query getaddrinfo with various address families and verify that
+ it returns EAI_AGAIN when DNS resolution fails. */
+static void
+query_host (const char *host_name)
+{
+ int family[] = { AF_INET, AF_INET6, AF_UNSPEC };
+ const char *family_names[] = { "AF_INET", "AF_INET6", "AF_UNSPEC" };
+
+ for (int i = 0; i < 3; i++)
+ {
+ struct addrinfo hints =
+ {
+ .ai_socktype = 0,
+ .ai_protocol = 0,
+ .ai_family = family[i],
+ .ai_flags = 0,
+ };
+ struct addrinfo *result;
+ int res = getaddrinfo (host_name, NULL, &hints, &result);
+ if (res != EAI_AGAIN)
+ FAIL_EXIT1 ("getaddrinfo (%s, %s) returned %s, expected EAI_AGAIN",
+ host_name, family_names[i], gai_strerror (res));
+ }
+}
+
+static int
+do_test (void)
+{
+ query_host ("site.example");
+ return 0;
+}
+
+#include <support/test-driver.c>

258
glibc-RHEL-153056-2.patch Normal file
View File

@ -0,0 +1,258 @@
commit c995686e2cbe2a3ab2a11877a61c14a2e1fc35cb
Author: Florian Weimer <fweimer@redhat.com>
Date: Tue Mar 3 18:48:47 2026 +0100
support: no_override_resolv_conf_search flag for resolver test framework
It is required to test "search ." in /etc/resolv.conf files. The
default is to override the search path isolate from unexpected
settings in the test execution environment.
Reviewed-by: Carlos O'Donell <carlos@redhat.com>
Conflicts:
resolv/Makefile
(tst-resolv-invalid-cname dependencies were added
in a different place on the 2.34 upstream branch)
diff --git a/resolv/Makefile b/resolv/Makefile
index cb1ff182dfdd4572..116ab6a1574eb1ff 100644
--- a/resolv/Makefile
+++ b/resolv/Makefile
@@ -83,6 +83,7 @@ tests = tst-aton tst-leaks tst-inet_ntop
tests-container += \
tst-getaddrinfo-eai-again \
tst-leaks2 \
+ tst-resolv-no-search \
# tests-container
tests-internal += tst-inet_aton_exact
@@ -294,10 +295,11 @@ $(objpfx)tst-resolv-res_init-multi: $(objpfx)libresolv.so \
$(shared-thread-library)
$(objpfx)tst-resolv-res_init-thread: $(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-invalid-cname: $(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)
$(objpfx)tst-resolv-nondecimal: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-qtypes: $(objpfx)libresolv.so $(shared-thread-library)
$(objpfx)tst-resolv-rotate: $(objpfx)libresolv.so $(shared-thread-library)
diff --git a/resolv/tst-resolv-no-search.c b/resolv/tst-resolv-no-search.c
new file mode 100644
index 0000000000000000..29701d4772507507
--- /dev/null
+++ b/resolv/tst-resolv-no-search.c
@@ -0,0 +1,174 @@
+/* Test using "search ." in /etc/resolv.conf.
+ 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 <netdb.h>
+#include <resolv.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/check_nss.h>
+#include <support/namespace.h>
+#include <support/resolv_test.h>
+#include <support/support.h>
+
+/* Check that plain res_init loads the configuration as expected. */
+static void
+test_res_init (void *ignored)
+{
+ res_init ();
+ TEST_COMPARE_STRING (_res.dnsrch[0], ".");
+ TEST_COMPARE_STRING (_res.dnsrch[1], NULL);
+}
+
+static void
+response (const struct resolv_response_context *ctx,
+ struct resolv_response_builder *b,
+ const char *qname, uint16_t qclass, uint16_t qtype)
+{
+ TEST_VERIFY_EXIT (qclass == C_IN);
+ TEST_COMPARE (ctx->server_index, 0);
+
+ if (strncmp (qname, "does-not-exist", strlen ("does-not-exist")) == 0)
+ {
+ resolv_response_init (b, (struct resolv_response_flags)
+ { .rcode = ns_r_nxdomain });
+ resolv_response_add_question (b, qname, qclass, qtype);
+ return;
+ }
+
+ resolv_response_init (b, (struct resolv_response_flags) { });
+ resolv_response_add_question (b, qname, qclass, qtype);
+ resolv_response_section (b, ns_s_an);
+
+ resolv_response_open_record (b, qname, qclass, qtype, 0);
+ switch (qtype)
+ {
+ case T_A:
+ {
+ char ipv4[4] = {192, 0, 2, 17};
+ resolv_response_add_data (b, &ipv4, sizeof (ipv4));
+ }
+ break;
+ case T_AAAA:
+ {
+ char ipv6[16]
+ = {0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
+ resolv_response_add_data (b, &ipv6, sizeof (ipv6));
+ }
+ break;
+ }
+ resolv_response_close_record (b);
+}
+
+static void
+check_h (const char *name, int family, const char *expected)
+{
+ if (family == AF_INET)
+ {
+ char *query = xasprintf ("gethostbyname (\"%s\")", name);
+ check_hostent (query, gethostbyname (name), expected);
+ free (query);
+ }
+ {
+ char *query = xasprintf ("gethostbyname2 (\"%s\", %d)", name, family);
+ check_hostent (query, gethostbyname2 (name, family), expected);
+ free (query);
+ }
+}
+
+static void
+check_ai (const char *name, int family, const char *expected)
+{
+ struct addrinfo hints = { .ai_family = family, .ai_socktype = SOCK_STREAM, };
+ struct addrinfo *ai;
+ char *query = xasprintf ("%s:80 [%d]", name, hints.ai_family);
+ int ret = getaddrinfo (name, "80", &hints, &ai);
+ check_addrinfo (query, ai, ret, expected);
+ if (ret == 0)
+ freeaddrinfo (ai);
+ free (query);
+}
+
+static int
+do_test (void)
+{
+ support_isolate_in_subprocess (test_res_init, NULL);
+
+ struct resolv_test *aux = resolv_test_start
+ ((struct resolv_redirect_config)
+ {
+ .response_callback = response,
+ .no_override_resolv_conf_search = true,
+ });
+
+ check_h ("www.example", AF_INET,
+ "name: www.example\n"
+ "address: 192.0.2.17\n");
+ check_h ("www.example", AF_INET6,
+ "name: www.example\n"
+ "address: 2001:db8::1\n");
+ check_ai ("www.example", AF_UNSPEC,
+ "address: STREAM/TCP 192.0.2.17 80\n"
+ "address: STREAM/TCP 2001:db8::1 80\n");
+ check_ai ("www.example", AF_INET,
+ "address: STREAM/TCP 192.0.2.17 80\n");
+ check_ai ("www.example", AF_INET6,
+ "address: STREAM/TCP 2001:db8::1 80\n");
+ check_h ("does-not-exist.example", AF_INET,
+ "error: HOST_NOT_FOUND\n");
+ check_h ("does-not-exist.example", AF_INET6,
+ "error: HOST_NOT_FOUND\n");
+ check_ai ("does-not-exist.example", AF_UNSPEC,
+ "error: Name or service not known\n");
+ check_ai ("does-not-exist.example", AF_INET,
+ "error: Name or service not known\n");
+ check_ai ("does-not-exist.example", AF_INET6,
+ "error: Name or service not known\n");
+
+ /* With trailing dot. */
+ check_h ("www.example.", AF_INET,
+ "name: www.example\n"
+ "address: 192.0.2.17\n");
+ check_h ("www.example.", AF_INET6,
+ "name: www.example\n"
+ "address: 2001:db8::1\n");
+ check_ai ("www.example.", AF_UNSPEC,
+ "address: STREAM/TCP 192.0.2.17 80\n"
+ "address: STREAM/TCP 2001:db8::1 80\n");
+ check_ai ("www.example.", AF_INET,
+ "address: STREAM/TCP 192.0.2.17 80\n");
+ check_ai ("www.example.", AF_INET6,
+ "address: STREAM/TCP 2001:db8::1 80\n");
+ check_h ("does-not-exist.example.", AF_INET,
+ "error: HOST_NOT_FOUND\n");
+ check_h ("does-not-exist.example.", AF_INET6,
+ "error: HOST_NOT_FOUND\n");
+ check_ai ("does-not-exist.example.", AF_UNSPEC,
+ "error: Name or service not known\n");
+ check_ai ("does-not-exist.example.", AF_INET,
+ "error: Name or service not known\n");
+ check_ai ("does-not-exist.example.", AF_INET6,
+ "error: Name or service not known\n");
+
+ resolv_test_end (aux);
+
+ return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/resolv/tst-resolv-no-search.root/etc/resolv.conf b/resolv/tst-resolv-no-search.root/etc/resolv.conf
new file mode 100644
index 0000000000000000..5ace648869a31edb
--- /dev/null
+++ b/resolv/tst-resolv-no-search.root/etc/resolv.conf
@@ -0,0 +1 @@
+search .
diff --git a/support/resolv_test.c b/support/resolv_test.c
index 73d20572f3b843c7..7272334b78fc85f1 100644
--- a/support/resolv_test.c
+++ b/support/resolv_test.c
@@ -1098,6 +1098,9 @@ resolv_test_init (void)
static void
set_search_path (struct resolv_redirect_config config)
{
+ if (config.no_override_resolv_conf_search)
+ return;
+
memset (_res.defdname, 0, sizeof (_res.defdname));
memset (_res.dnsrch, 0, sizeof (_res.dnsrch));
diff --git a/support/resolv_test.h b/support/resolv_test.h
index ddf967449e398710..e0a17ebf23c34880 100644
--- a/support/resolv_test.h
+++ b/support/resolv_test.h
@@ -96,6 +96,9 @@ struct resolv_redirect_config
domain name as well. */
const char *search[7];
+ /* If true, do not override the search path loaded from /etc/resolv.conf. */
+ bool no_override_resolv_conf_search;
+
/* Number of servers to activate in resolv. 0 means the default,
resolv_max_test_servers. */
int nscount;

98
glibc-RHEL-153056-3.patch Normal file
View File

@ -0,0 +1,98 @@
commit dc9ca785a5fe2059a9b04ab336520d463d9a715b
Author: Carlos Peón Costa <carlospeon@gmail.com>
Date: Tue Mar 3 18:48:47 2026 +0100
resolv: Avoid duplicate query if search list contains '.' (bug 33804)
Co-authored-by: Florian Weimer <fweimer@redhat.com>
Signed-off-by: Florian Weimer <fweimer@redhat.com>
Reviewed-by: Carlos O'Donell <carlos@redhat.com>
diff --git a/resolv/res_query.c b/resolv/res_query.c
index 1d2c81737bc889c9..bae7b3c23d778a7d 100644
--- a/resolv/res_query.c
+++ b/resolv/res_query.c
@@ -358,7 +358,7 @@ __res_context_search (struct resolv_context *ctx,
char tmp[NS_MAXDNAME];
u_int dots;
int trailing_dot, ret, saved_herrno;
- int got_nodata = 0, got_servfail = 0, root_on_list = 0;
+ int got_nodata = 0, got_servfail = 0;
int tried_as_is = 0;
int searched = 0;
@@ -437,8 +437,11 @@ __res_context_search (struct resolv_context *ctx,
domain. */
if (dname[0] == '.')
dname++;
- if (dname[0] == '\0')
- root_on_list++;
+ if (dname[0] == '\0') {
+ if (tried_as_is)
+ continue;
+ tried_as_is++;
+ }
ret = __res_context_querydomain
(ctx, name, dname, class, type,
@@ -510,7 +513,7 @@ __res_context_search (struct resolv_context *ctx,
* unless RES_NOTLDQUERY is set and there were no dots.
*/
if ((dots || !searched || (statp->options & RES_NOTLDQUERY) == 0)
- && !(tried_as_is || root_on_list)) {
+ && !tried_as_is) {
ret = __res_context_querydomain
(ctx, name, NULL, class, type,
answer, anslen, answerp, answerp2, nanswerp2,
diff --git a/resolv/tst-resolv-no-search.c b/resolv/tst-resolv-no-search.c
index 29701d4772507507..7d78d4044cf50e48 100644
--- a/resolv/tst-resolv-no-search.c
+++ b/resolv/tst-resolv-no-search.c
@@ -27,6 +27,11 @@
#include <support/resolv_test.h>
#include <support/support.h>
+/* Used to check for duplicated queries (bug 33804). POSIX does not
+ explicitly say that socket calls (as used in the resolver tests)
+ provide synchronization. */
+static _Atomic unsigned int query_count;
+
/* Check that plain res_init loads the configuration as expected. */
static void
test_res_init (void *ignored)
@@ -43,6 +48,7 @@ response (const struct resolv_response_context *ctx,
{
TEST_VERIFY_EXIT (qclass == C_IN);
TEST_COMPARE (ctx->server_index, 0);
+ ++query_count;
if (strncmp (qname, "does-not-exist", strlen ("does-not-exist")) == 0)
{
@@ -82,12 +88,16 @@ check_h (const char *name, int family, const char *expected)
if (family == AF_INET)
{
char *query = xasprintf ("gethostbyname (\"%s\")", name);
+ query_count = 0;
check_hostent (query, gethostbyname (name), expected);
+ TEST_COMPARE (query_count, 1);
free (query);
}
{
char *query = xasprintf ("gethostbyname2 (\"%s\", %d)", name, family);
+ query_count = 0;
check_hostent (query, gethostbyname2 (name, family), expected);
+ TEST_COMPARE (query_count, 1);
free (query);
}
}
@@ -98,8 +108,10 @@ check_ai (const char *name, int family, const char *expected)
struct addrinfo hints = { .ai_family = family, .ai_socktype = SOCK_STREAM, };
struct addrinfo *ai;
char *query = xasprintf ("%s:80 [%d]", name, hints.ai_family);
+ query_count = 0;
int ret = getaddrinfo (name, "80", &hints, &ai);
check_addrinfo (query, ai, ret, expected);
+ TEST_COMPARE (query_count, family == AF_UNSPEC ? 2 : 1);
if (ret == 0)
freeaddrinfo (ai);
free (query);