[Dnsmasq-discuss] [PATCH] Close listening socket on fatal accept() errors to prevent busy loop

zhou yuefu yuefu16.zhou at gmail.com
Thu Aug 20 06:52:19 UTC 2026


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 yuefu16.zhou at gmail.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/attachments/20260820/78c06432/attachment.htm>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: 0001-Close-listening-socket-on-fatal-accept-errors-to-pre.patch
Type: text/x-patch
Size: 2113 bytes
Desc: not available
URL: <http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/attachments/20260820/78c06432/attachment.bin>


More information about the Dnsmasq-discuss mailing list