[PATCH v2] resolv: reset _u._ext.nscount in __res_iclose [BZ #34154]
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Mon Jun 1 20:27:43 GMT 2026
On 18/05/26 00:21, Adam Yi wrote:
> __res_iclose, when called with FREE_ADDR=true, frees and NULLs every
> statp->_u._ext.nsaddrs[ns] but does not reset statp->_u._ext.nscount.
> This breaks the invariant relied on by __res_context_send's cache
> validation loop in resolv/res_send.c:293-312: when _u._ext.nscount
> is non-zero, every _u._ext.nsaddrs[ns] corresponding to
> statp->nsaddr_list[ns].sin_family != 0 is expected to be non-NULL.
>
> res_init unconditionally calls __res_iclose(&_res, true) before
> __res_vinit, so any __res_vinit failure (for example,
> fopen("/etc/resolv.conf") returning EMFILE under file-descriptor
> exhaustion, or any allocation failure in __resolv_conf_load,
> __resolv_conf_allocate, or __resolv_conf_attach before
> update_from_conf runs) leaves _u._ext.nscount non-zero with all
> _u._ext.nsaddrs[] NULL. The next name lookup walks the validation
> loop and dereferences NULL in sock_eq. A DNS lookup may fail after
> a failed res_init, but it should not segfault.
>
> Reset _u._ext.nscount = 0 alongside the existing __resolv_conf_detach
> call. The next __res_context_send call then re-enters its init
> block (res_send.c:316-335) and repopulates _u._ext.nsaddrs[] from
> statp->nsaddr_list[], which __res_iclose leaves untouched. This
> also lets support/resolv_test.c drop its now-redundant manual reset
> after __res_iclose(&_res, true).
>
> Add a regression test that drops RLIMIT_NOFILE so fopen of
> /etc/resolv.conf fails with EMFILE inside __res_vinit, then verifies
> the subsequent gethostbyname does not crash.
>
> Signed-off-by: Adam Yi <ayi@janestreet.com>
> ---
> Changes since v1:
> - Use the DCO copyright statement ("Copyright The GNU Toolchain
> Authors.") on the new test file, per Florian's review.
> - Add Signed-off-by trailer (DCO sign-off).
> No code changes.
>
> Tested on x86_64-linux-gnu against current master (commit
> 3bd334037e). The new regression test fails with SIGSEGV without
> the res-close.c hunk and passes with it; no pre-existing resolv/
> tests are regressed.
>
> The specific NULL-dereference in __res_context_send's validation
> loop was introduced by commit 2212c1420c (2015-02-19), which
> switched the cache-valid flag from _u._ext.nsinit to
> _u._ext.nscount but did not add a matching reset of the new flag
> in __res_iclose. Affects glibc 2.22 (August 2015) through master.
>
> Related fixes in the same area, both for "resolver state left
> inconsistent under an allocation/initialization failure" but at
> different sites: BZ #23005 (commit f178e59fa5, partial init block
> in __res_context_send when malloc fails) and BZ #30527 (commit
> abcf8db7fa, lock release in resolv_conf).
LGTM, thanks.
Reviewed-by: Adhemerval Zanella <adhemerval.zanella@linaro.org>
>
> resolv/Makefile | 3 +
> resolv/res-close.c | 9 ++-
> resolv/tst-resolv-res_init-failure.c | 109 +++++++++++++++++++++++++++
> support/resolv_test.c | 1 -
> 4 files changed, 119 insertions(+), 3 deletions(-)
> create mode 100644 resolv/tst-resolv-res_init-failure.c
>
> diff --git a/resolv/Makefile b/resolv/Makefile
> index 971608eff5..68b3a4dbf3 100644
> --- a/resolv/Makefile
> +++ b/resolv/Makefile
> @@ -122,6 +122,7 @@ tests += \
> tst-resolv-noaaaa \
> tst-resolv-noaaaa-vc \
> tst-resolv-nondecimal \
> + tst-resolv-res_init-failure \
> tst-resolv-res_init-multi \
> tst-resolv-search \
> tst-resolv-semi-failure \
> @@ -307,6 +308,8 @@ $(objpfx)tst-resolv-dns-section: $(objpfx)libresolv.so \
> $(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
> +$(objpfx)tst-resolv-res_init-failure: $(objpfx)libresolv.so \
> + $(shared-thread-library)
> $(objpfx)tst-resolv-res_init-multi: $(objpfx)libresolv.so \
> $(shared-thread-library)
> $(objpfx)tst-resolv-res_init-thread: $(objpfx)libresolv.so \
> diff --git a/resolv/res-close.c b/resolv/res-close.c
> index 374f09d2de..a6aaa22aaf 100644
> --- a/resolv/res-close.c
> +++ b/resolv/res-close.c
> @@ -89,7 +89,9 @@
> #include <stdlib.h>
>
> /* Close all open sockets. If FREE_ADDR is true, deallocate any
> - separately allocated name server addresses. */
> + separately allocated name server addresses, detach the extended
> + resolver configuration, and reset the cached extended-state count
> + so subsequent name lookups reinitialize the extended state. */
> void
> __res_iclose (res_state statp, bool free_addr)
> {
> @@ -114,7 +116,10 @@ __res_iclose (res_state statp, bool free_addr)
> }
> }
> if (free_addr)
> - __resolv_conf_detach (statp);
> + {
> + __resolv_conf_detach (statp);
> + statp->_u._ext.nscount = 0;
> + }
> }
> libc_hidden_def (__res_iclose)
>
> diff --git a/resolv/tst-resolv-res_init-failure.c b/resolv/tst-resolv-res_init-failure.c
> new file mode 100644
> index 0000000000..d20348acf2
> --- /dev/null
> +++ b/resolv/tst-resolv-res_init-failure.c
> @@ -0,0 +1,109 @@
> +/* Test that name lookups do not crash after a failing res_init call.
> + Copyright The GNU Toolchain Authors.
> + 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/>. */
> +
> +/* When res_init is called on a thread whose resolver state has
> + already been initialized, it unconditionally invokes
> + __res_iclose(&_res, true) before __res_vinit. __res_iclose used
> + to NULL every _u._ext.nsaddrs[ns] without resetting
> + _u._ext.nscount, breaking the invariant that
> + __res_context_send's validation loop relies on. If __res_vinit
> + then failed (for example, because fopen("/etc/resolv.conf")
> + returned EMFILE), the resolver state was left with
> + _u._ext.nscount > 0 and _u._ext.nsaddrs[*] == NULL. The next
> + name lookup walked the validation loop and dereferenced NULL in
> + sock_eq.
> +
> + This test reproduces the crash:
> + 1. perform a name lookup so __res_context_send populates
> + _u._ext.nsaddrs[] and sets _u._ext.nscount;
> + 2. drop RLIMIT_NOFILE so fopen("/etc/resolv.conf") fails with
> + EMFILE on the next call;
> + 3. call res_init, which must report failure;
> + 4. perform another name lookup, which must not crash. */
> +
> +#include <arpa/nameser.h>
> +#include <netdb.h>
> +#include <resolv.h>
> +#include <sys/resource.h>
> +
> +#include <support/check.h>
> +#include <support/resolv_test.h>
> +#include <support/xunistd.h>
> +
> +static void
> +response (const struct resolv_response_context *ctx,
> + struct resolv_response_builder *b,
> + const char *qname, uint16_t qclass, uint16_t qtype)
> +{
> + resolv_response_init (b, (struct resolv_response_flags) { 0 });
> + resolv_response_add_question (b, qname, qclass, qtype);
> + resolv_response_section (b, ns_s_an);
> + resolv_response_open_record (b, qname, qclass, qtype, 0);
> + if (qtype == T_A)
> + {
> + const char ipv4[4] = { 192, 0, 2, 1 };
> + resolv_response_add_data (b, ipv4, sizeof (ipv4));
> + }
> + resolv_response_close_record (b);
> +}
> +
> +static int
> +do_test (void)
> +{
> + struct resolv_test *aux = resolv_test_start
> + ((struct resolv_redirect_config) { .response_callback = response });
> +
> + /* Initial lookup. This drives __res_context_send through its init
> + block, which allocates _u._ext.nsaddrs[] and sets
> + _u._ext.nscount to _res.nscount. */
> + struct hostent *h = gethostbyname ("primer.example");
> + TEST_VERIFY_EXIT (h != NULL);
> +
> + /* Drop RLIMIT_NOFILE so that the next fopen of /etc/resolv.conf
> + inside __resolv_conf_load fails with EMFILE. Already-open
> + descriptors (including those held by the resolv test harness)
> + remain valid; only new descriptor allocations are blocked. */
> + struct rlimit rl;
> + TEST_COMPARE (getrlimit (RLIMIT_NOFILE, &rl), 0);
> + rlim_t saved_cur = rl.rlim_cur;
> + rl.rlim_cur = 3;
> + TEST_COMPARE (setrlimit (RLIMIT_NOFILE, &rl), 0);
> +
> + /* res_init must report failure: __res_iclose has already cleared
> + the extended state, __res_vinit then fails to reload
> + /etc/resolv.conf. */
> + TEST_COMPARE (res_init (), -1);
> +
> + /* The repro: without the fix in __res_iclose, this call would
> + enter __res_context_send's validation loop with stale
> + _u._ext.nscount and NULL _u._ext.nsaddrs[0], dereference NULL
> + in sock_eq, and SIGSEGV. We do not care whether the lookup
> + succeeds (it will likely fail because socket() also returns
> + EMFILE), only that it returns without crashing. */
> + (void) gethostbyname ("after.example");
> +
> + /* Restore the descriptor limit before resolv_test_end so its
> + cleanup work is not constrained. */
> + rl.rlim_cur = saved_cur;
> + TEST_COMPARE (setrlimit (RLIMIT_NOFILE, &rl), 0);
> +
> + resolv_test_end (aux);
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/support/resolv_test.c b/support/resolv_test.c
> index dad755ac30..ec406b281a 100644
> --- a/support/resolv_test.c
> +++ b/support/resolv_test.c
> @@ -1183,7 +1183,6 @@ resolv_test_start (struct resolv_redirect_config config)
> /* Disable IPv6 name server addresses. The code below only
> overrides the IPv4 addresses. */
> __res_iclose (&_res, true);
> - _res._u._ext.nscount = 0;
>
> /* Redirect queries to the server socket. */
> if (test_verbose)
More information about the Libc-alpha
mailing list