Hi Simon,

  If a TCP listening socket is externally destroyed (e.g., via ss -K,
  or a process using NETLINK_SOCK_DIAG/SOCK_DESTROY), accept()
  permanently returns -1 with errno == EINVAL because the socket is no
  longer in TCP_LISTEN state. Since poll() keeps reporting the stale fd
  as readable, the main loop spins calling do_tcp_connection() ->
  accept() indefinitely, consuming 100% CPU.

  Reproduce:

  $ dnsmasq --bind-dynamic --interface=lo --port=15353 --no-daemon &
  $ sudo ss -K state listening sport = :15353
  $ # dnsmasq now spins at 100% CPU

  The fix distinguishes transient errors from fatal ones. On transient
  errors (EAGAIN, ECONNABORTED, EMFILE, ENFILE, ENOMEM, ENOBUFS), we
  just return and retry on the next poll cycle. On fatal errors (EINVAL,
  EBADF, etc.), we close the tcpfd and mark it -1, so poll() no longer
  selects it.

  In --bind-dynamic mode, the listener will be automatically rebuilt on
  the next address change event via newaddress(). Alternatively, a
  dedicated event could trigger targeted listener rebuild without the
  DHCPv6/RA side effects of EVENT_NEWADDR — happy to implement that if
  you prefer.

  Tested: dnsmasq 2.93 on Linux 6.1/5.15, verified 0% CPU after socket
  destruction with the patch (vs 100% without).

  diff --git a/src/dnsmasq.c b/src/dnsmasq.c
  index c1e48fc..fa4a467 100644
  --- a/src/dnsmasq.c
  +++ b/src/dnsmasq.c
  @@ -2029,7 +2029,20 @@ static void do_tcp_connection(struct listener
*listener, time_t now, int slot)
     while ((confd = accept(listener->tcpfd, NULL, NULL)) == -1 && errno ==
EINTR);

     if (confd == -1)
  -    return;
  +    {
  +      /* Transient errors: just return and retry on next poll cycle. */
  +      if (errno == EAGAIN || errno == ECONNABORTED ||
  +          errno == EMFILE || errno == ENFILE ||
  +          errno == ENOMEM || errno == ENOBUFS)
  +        return;
  +
  +      /* Fatal error (EINVAL, EBADF, etc): socket is permanently broken.
  +         Close it so poll() no longer selects it.  In --bind-dynamic mode
  +         the listener will be rebuilt on the next address change event.
*/
  +      close(listener->tcpfd);
  +      listener->tcpfd = -1;
  +      return;
  +    }

     if (getsockname(confd, (struct sockaddr *)&tcp_addr, &tcp_len) == -1)
       {

  Signed-off-by: Yuefu Zhou [email protected]
From 3935e0762c37b0e9a5eb5a7705b75d35046b47b4 Mon Sep 17 00:00:00 2001
From: Yuefu Zhou <[email protected]>
Date: Thu, 20 Aug 2026 14:39:41 +0800
Subject: [PATCH] Close listening socket on fatal accept() errors to prevent
 busy loop

If a TCP listening socket is externally destroyed (e.g., via ss -K,
or a process using NETLINK_SOCK_DIAG/SOCK_DESTROY), accept()
permanently returns -1 with errno == EINVAL because the socket is
no longer in TCP_LISTEN state.  Since poll() keeps reporting the
stale fd as readable, the main loop spins calling
do_tcp_connection() -> accept() indefinitely, consuming 100% CPU.

Distinguish transient errors (EAGAIN, ECONNABORTED, EMFILE, ENFILE,
ENOMEM, ENOBUFS) from fatal ones.  On transient errors just return
and retry on the next poll cycle.  On fatal errors close the tcpfd
and mark it -1 so poll() no longer selects it.

In --bind-dynamic mode the listener will be automatically rebuilt
on the next address change event via newaddress().

Signed-off-by: Yuefu Zhou <[email protected]>
---
 src/dnsmasq.c | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/src/dnsmasq.c b/src/dnsmasq.c
index c1e48fc..fa4a467 100644
--- a/src/dnsmasq.c
+++ b/src/dnsmasq.c
@@ -2029,7 +2029,20 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot)
   while ((confd = accept(listener->tcpfd, NULL, NULL)) == -1 && errno == EINTR);
   
   if (confd == -1)
-    return;
+    {
+      /* Transient errors: just return and retry on next poll cycle. */
+      if (errno == EAGAIN || errno == ECONNABORTED ||
+          errno == EMFILE || errno == ENFILE ||
+          errno == ENOMEM || errno == ENOBUFS)
+        return;
+
+      /* Fatal error (EINVAL, EBADF, etc): socket is permanently broken.
+         Close it so poll() no longer selects it.  In --bind-dynamic mode
+         the listener will be rebuilt on the next address change event. */
+      close(listener->tcpfd);
+      listener->tcpfd = -1;
+      return;
+    }
   
   if (getsockname(confd, (struct sockaddr *)&tcp_addr, &tcp_len) == -1)
     {
-- 
2.25.1

_______________________________________________
Dnsmasq-discuss mailing list
[email protected]
https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss

Reply via email to