Fix #2431: drop getaddrinfo_a path to eliminate stack-use-after-free (#2436)

The Linux/glibc branch of detail::getaddrinfo_with_timeout used
getaddrinfo_a(GAI_NOWAIT) with a stack-local struct gaicb. On the
connection-timeout branch it called gai_cancel(), which is non-blocking
and may return EAI_NOTCANCELED -- in that case the resolver worker
thread is still alive and writes back to ar_result on the now-destroyed
stack frame after the function has already returned.

Drop the entire #elif _GNU_SOURCE && __GLIBC__ branch and let glibc
fall through to the existing std::thread + std::shared_ptr<State>
implementation that the file already uses for other Unix systems. That
path captures shared ownership in the resolver lambda, so the state
outlives the caller's frame whether or not the worker finishes in
time -- no stack frame is ever referenced after return.

The reproducer added in #2433 (issue-2431 repro CI job) goes from
hanging at job teardown to passing in ~25s with this change.
This commit is contained in:
yhirose
2026-04-28 18:34:14 +09:00
committed by GitHub
parent d14e4fc05f
commit 5ebbfeef0b
+10 -51
View File
@@ -5905,58 +5905,17 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
*res = result_addrinfo;
return 0;
#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \
(__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2))
// Linux implementation using getaddrinfo_a for asynchronous DNS resolution
struct gaicb request;
struct gaicb *requests[1] = {&request};
struct sigevent sevp;
struct timespec timeout;
// Initialize the request structure
memset(&request, 0, sizeof(request));
request.ar_name = node;
request.ar_service = service;
request.ar_request = hints;
// Set up timeout
timeout.tv_sec = timeout_sec;
timeout.tv_nsec = 0;
// Initialize sigevent structure (not used, but required)
memset(&sevp, 0, sizeof(sevp));
sevp.sigev_notify = SIGEV_NONE;
// Start asynchronous resolution
int start_result = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp);
if (start_result != 0) { return start_result; }
// Wait for completion with timeout
int wait_result =
gai_suspend((const struct gaicb *const *)requests, 1, &timeout);
if (wait_result == 0 || wait_result == EAI_ALLDONE) {
// Completed successfully, get the result
int gai_result = gai_error(&request);
if (gai_result == 0) {
*res = request.ar_result;
return 0;
} else {
// Clean up on error
if (request.ar_result) { freeaddrinfo(request.ar_result); }
return gai_result;
}
} else if (wait_result == EAI_AGAIN) {
// Timeout occurred, cancel the request
gai_cancel(&request);
return EAI_AGAIN;
} else {
// Other error occurred
gai_cancel(&request);
return wait_result;
}
#else
// Fallback implementation using thread-based timeout for other Unix systems
// Fallback implementation using thread-based timeout for other Unix systems.
//
// The previous Linux/glibc path used getaddrinfo_a(GAI_NOWAIT) with a
// stack-local gaicb. On timeout it called gai_cancel(), which is non-
// blocking and may return EAI_NOTCANCELED -- the resolver worker would
// then write back to the destroyed stack frame after this function had
// already returned (#2431). The std::thread + shared_ptr fallback below
// is correct for the same reason it works on other platforms: the
// worker captures shared ownership of the state, so it stays alive as
// long as anyone (caller or worker) still holds a reference.
struct GetAddrInfoState {
~GetAddrInfoState() {