Add timeout for udp dns requests to avoid resource leak (#41395)

The tunneled UDP DNS request could leak if the Windows side produces no response. This PR adds a 60s timeout for each request to avoid this potential leak.

Feng Wang committed Aug 21, 2026 at 10:26 UTC efa237421c17bcbb49239d838d2250ab53d8b4a3
2 files changed +59 -7
src/linux/init/DnsServer.cpp
+43 -6
@@ -4,8 +4,8 @@
4 #include <sys/epoll.h>
5 #include <netinet/in.h>
6 #include <sys/socket.h>
7 -#include "DnsServer.h"
7 #include "RuntimeErrorWithSourceLocation.h"
8 +#include "DnsServer.h"
9 #include "Syscall.h"
10 #include "util.h"
11
@@ -15,6 +15,8 @@ constexpr int c_dnsServerPort = 53;
15 constexpr int c_epollWaitMaxEvents = 100;
16 // Maximum size of DNS over UDP requests is 4096 bytes (max size is reached for EDNS UDP requests)
17 constexpr int c_maxUdpDnsBufferSize = 4096;
18 +// Maximum time to wait for a tunneled UDP DNS response
19 +constexpr auto c_udpRequestTimeout = std::chrono::seconds{60};
20 // Max number of pending connections in the TCP listen queue
21 constexpr int c_maxListenBacklog = 1000;
22
@@ -119,9 +121,12 @@ try
121 }
122
123 // Stop tracking the request, irrespective of the DNS response being successfully sent
122 - const auto removeDnsRequest = wil::scope_exit([&] { m_udpRequests.erase(dnsClientIdentifier.DnsClientId); });
124 + const auto removeDnsRequest = wil::scope_exit([&] {
125 + m_udpRequestExpirations.erase(it->second.m_expiration);
126 + m_udpRequests.erase(it);
127 + });
128
124 - sockaddr_in& remoteAddr = it->second;
129 + sockaddr_in& remoteAddr = it->second.m_remoteAddress;
130
131 // Send DNS response buffer back to the Linux DNS client
132 int bufferSize = dnsBuffer.size();
@@ -298,6 +303,25 @@ try
303 }
304 CATCH_LOG()
305
306 +int DnsServer::ExpireUdpRequestsAndGetTimeout() noexcept
307 +{
308 + std::scoped_lock<std::mutex> lock{m_udpLock};
309 + const auto now = std::chrono::steady_clock::now();
310 +
311 + while (!m_udpRequestExpirations.empty() && m_udpRequestExpirations.front().first <= now)
312 + {
313 + m_udpRequests.erase(m_udpRequestExpirations.front().second);
314 + m_udpRequestExpirations.pop_front();
315 + }
316 +
317 + if (m_udpRequestExpirations.empty())
318 + {
319 + return -1;
320 + }
321 +
322 + return static_cast<int>(std::chrono::ceil<std::chrono::milliseconds>(m_udpRequestExpirations.front().first - now).count());
323 +}
324 +
325 void DnsServer::ServerLoop() noexcept
326 {
327 UtilSetThreadName("DnsServer");
@@ -311,7 +335,8 @@ void DnsServer::ServerLoop() noexcept
335 {
336 // A fixed number of events is requested from epoll_wait (c_epollWaitMaxEvents). In case the number of ready events is
337 // greater than c_epollWaitMaxEvents, epoll will round-robin through the ready events until we get a notification for all of them.
314 - size_t numReadyEvents = Syscall(epoll_wait, m_epollFd.get(), events, c_epollWaitMaxEvents, -1);
338 + const auto timeout = ExpireUdpRequestsAndGetTimeout();
339 + size_t numReadyEvents = Syscall(epoll_wait, m_epollFd.get(), events, c_epollWaitMaxEvents, timeout);
340
341 // No event
342 if (numReadyEvents == 0)
@@ -388,14 +413,26 @@ try
413 udpRequestId = requestId;
414
415 // Track the request
391 - m_udpRequests.emplace(requestId, remoteAddr);
416 + const auto expiration = std::chrono::steady_clock::now() + c_udpRequestTimeout;
417 + const auto expirationIt = m_udpRequestExpirations.emplace(m_udpRequestExpirations.end(), expiration, requestId);
418 + auto removeExpirationOnError = wil::scope_exit([&] { m_udpRequestExpirations.erase(expirationIt); });
419 +
420 + const auto [_, inserted] = m_udpRequests.emplace(requestId, UdpRequestContext{remoteAddr, expirationIt});
421 + THROW_UNEXPECTED_IF(!inserted);
422 +
423 + removeExpirationOnError.release();
424 }
425
426 if (!dnsRequest.empty())
427 {
428 auto removeRequestOnError = wil::scope_exit([&] {
429 std::scoped_lock<std::mutex> lock{m_udpLock};
398 - m_udpRequests.erase(udpRequestId);
430 + const auto it = m_udpRequests.find(udpRequestId);
431 + if (it != m_udpRequests.end())
432 + {
433 + m_udpRequestExpirations.erase(it->second.m_expiration);
434 + m_udpRequests.erase(it);
435 + }
436 });
437
438 // Tunnel request to Windows
src/linux/init/DnsServer.h
+16 -1
@@ -2,6 +2,8 @@
2
3 #pragma once
4
5 +#include <chrono>
6 +#include <list>
7 #include <map>
8 #include "common.h"
9 #include "lxinitshared.h"
@@ -68,6 +70,14 @@ private:
70 TcpConnectionContext& operator=(TcpConnectionContext&&) = delete;
71 };
72
73 + using UdpRequestExpirationQueue = std::list<std::pair<std::chrono::steady_clock::time_point, uint32_t>>;
74 +
75 + struct UdpRequestContext
76 + {
77 + sockaddr_in m_remoteAddress;
78 + UdpRequestExpirationQueue::iterator m_expiration;
79 + };
80 +
81 void StartUdpDnsServer(const std::string& ipAddress) noexcept;
82
83 void StartTcpDnsServer(const std::string& ipAddress) noexcept;
@@ -84,6 +94,8 @@ private:
94 // Read the next DNS request from the UDP socket.
95 void HandleUdpDnsRequest() noexcept;
96
97 + int ExpireUdpRequestsAndGetTimeout() noexcept;
98 +
99 void HandleUdpDnsResponse(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
100
101 void HandleTcpDnsResponse(const gsl::span<gsl::byte> dnsBuffer, const LX_GNS_DNS_CLIENT_IDENTIFIER& dnsClientIdentifier) noexcept;
@@ -105,7 +117,10 @@ private:
117 // Mapping id of an UDP DNS request to the sockaddr_in struct storing the IP and port used by the Linux DNS client that made
118 // the DNS request. Note: Since we only configure an IPv4 DNS server in Linux, we expect all Linux DNS clients to use IPv4
119 // addresses. _Guarded_by_(m_udpLock)
108 - std::map<uint32_t, sockaddr_in> m_udpRequests;
120 + std::map<uint32_t, UdpRequestContext> m_udpRequests;
121 +
122 + // UDP requests ordered by expiration time. _Guarded_by_(m_udpLock)
123 + UdpRequestExpirationQueue m_udpRequestExpirations;
124
125 wil::unique_fd m_tcpListenSocket;
126