From 1da5cc2951d345efa2ed7206dbe86d201d9f2fe4 Mon Sep 17 00:00:00 2001 From: Zhou Yuefu Date: Tue, 7 Jul 2026 11:13:44 +0800 Subject: [PATCH] Close inherited listening sockets in forked TCP child processes When dnsmasq forks a child to handle a TCP connection, the child inherits copies of all listening sockets. These are never used but keep the underlying sockets alive in the kernel. If a network interface is removed and re-added while a child is running, the parent's attempt to re-bind fails with EADDRINUSE because the child still holds a reference. Close all listener fds (UDP and TCP) in the child immediately after fork in both do_tcp_connection() and swap_to_tcp(). [Original patch extended by Simon Kelley to include the TFTP listening socket, and to extend the existing race-protection scheme for the netlink socket to the listening sockets. Any bugs are my responsibility.] --- src/dnsmasq.c | 111 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 37 deletions(-) diff --git a/src/dnsmasq.c b/src/dnsmasq.c index adaa692..8375f42 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -2015,7 +2015,8 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) pid_t p; union mysockaddr tcp_addr; socklen_t tcp_len = sizeof(union mysockaddr); - struct server *s; + struct server *s; + struct listener *l; int flags, auth_dns = 0; struct in_addr netmask; int pipefd[2]; @@ -2138,25 +2139,19 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) /* fork() done: parent side */ close(pipefd[1]); /* parent needs read pipe end. */ -#ifdef HAVE_LINUX_NETWORK - /* The child process inherits the netlink socket, - which it never uses, but when the parent (us) - uses it in the future, the answer may go to the - child, resulting in the parent blocking - forever awaiting the result. To avoid this - the child closes the netlink socket, but there's - a nasty race, since the parent may use netlink - before the child has done the close. + /* The child process inherits various sockets, + whose existence can mess up the function of + the parent. See below for details of which + sockets, and why they cause problems. The + child closes the sockets during startup, + but there's a nasty race, since the parent + may use them before the child has done the close. - To avoid this, the parent blocks here until a + To avoid the race, the parent blocks here until a single byte comes back up the pipe, which - is sent by the child after it has closed the - netlink socket. */ - + is sent by the child has finshed the close. */ read_write(pipefd[0], &a, 1, RW_READ); -#endif - daemon->tcp_pids[slot] = p; daemon->tcp_pipes[slot] = pipefd[0]; daemon->metrics[METRIC_TCP_CONNECTIONS]++; @@ -2176,18 +2171,41 @@ static void do_tcp_connection(struct listener *listener, time_t now, int slot) return; } +#ifdef HAVE_LINUX_NETWORK + /* The child process inherits the netlink socket, + which it never uses, but when the parent + uses it in the future, the answer may go to the + child, resulting in the parent blocking + forever awaiting the result. To avoid this + close the netlink socket in the child. */ + close(daemon->netlinkfd); +#endif + + /* Close inherited listening sockets in the child process. + These are not needed here and holding them prevents the + parent from re-binding if an interface is removed and + re-added (the child's copy causes EADDRINUSE). */ + for (l = daemon->listeners; l; l = l->next) + { + if (l->fd != -1) + close(l->fd); + if (l->tcpfd != -1) + close(l->tcpfd); + if (l->tftpfd != -1) + close(l->tftpfd); + } + + /* unblock parent now we're not holding sockets we shouldn't. */ + read_write(pipefd[1], &a, 1, RW_WRITE); + /* Arrange for SIGALRM after CHILD_LIFETIME seconds to terminate the process. */ -#ifdef HAVE_LINUX_NETWORK - /* See comment above re: netlink socket. */ - close(daemon->netlinkfd); - read_write(pipefd[1], &a, 1, RW_WRITE); -#endif alarm(CHILD_LIFETIME); close(pipefd[0]); /* close read end in child. */ daemon->pipe_to_parent = pipefd[1]; - } + } + /* The connected socket inherits non-blocking attribute from the listening socket. Reset that here. */ @@ -2232,6 +2250,7 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header ssize_t *plen, char *name, int class, struct server *server, int *keycount, int *validatecount) { struct server *s; + struct listener *l; if (!option_bool(OPT_DEBUG)) { @@ -2260,22 +2279,18 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header return STAT_ABANDONED; } -#ifdef HAVE_LINUX_NETWORK - /* The child process inherits the netlink socket, - which it never uses, but when the parent (us) - uses it in the future, the answer may go to the - child, resulting in the parent blocking - forever awaiting the result. To avoid this - the child closes the netlink socket, but there's - a nasty race, since the parent may use netlink - before the child has done the close. + /* The child process inherits various sockets, + whose existence can mess up the function of + the parent. See below for details of which + sockets, and why they cause problems. The + child closes the sockets during startup, + but there's a nasty race, since the parent + may use them before the child has done the close. - To avoid this, the parent blocks here until a + To avoid the race, the parent blocks here until a single byte comes back up the pipe, which - is sent by the child after it has closed the - netlink socket. */ + is sent by the child has finshed the close. */ read_write(pipefd[0], &a, 1, RW_READ); -#endif /* i holds index of free slot */ daemon->tcp_pids[i] = p; @@ -2294,10 +2309,32 @@ int swap_to_tcp(struct frec *forward, time_t now, int status, struct dns_header { /* child starts here. */ #ifdef HAVE_LINUX_NETWORK - /* See comment above re: netlink socket. */ + /* The child process inherits the netlink socket, + which it never uses, but when the parent + uses it in the future, the answer may go to the + child, resulting in the parent blocking + forever awaiting the result. To avoid this + close the netlink socket in the child. */ close(daemon->netlinkfd); +#endif + + /* Close inherited listening sockets in the child process. + These are not needed here and holding them prevents the + parent from re-binding if an interface is removed and + re-added (the child's copy causes EADDRINUSE). */ + for (l = daemon->listeners; l; l = l->next) + { + if (l->fd != -1) + close(l->fd); + if (l->tcpfd != -1) + close(l->tcpfd); + if (l->tftpfd != -1) + close(l->tftpfd); + } + + /* unblock parent now we're not holding sockets we shouldn't. */ read_write(pipefd[1], &a, 1, RW_WRITE); -#endif + alarm(CHILD_LIFETIME); close(pipefd[0]); /* close read end in child. */ daemon->pipe_to_parent = pipefd[1];