Subject: [RFC PATCH] Publish interface state only after a complete scan

Build a private candidate for interface records, auxiliary address lists,
configuration-use flags and server interface indexes. Publish it only
after both address-family scans succeed. Preserve live irec addresses at
commit, so listener pointers remain valid. On failure, discard the
candidate, retain the previous state and defer listener reconciliation.

Reject interrupted or failed netlink dumps, including NLM_F_DUMP_INTR,
current-request errors, DONE errors and receive errors. Preserve the
kernel-origin check and distinguish asynchronous address notifications
from stale replies to our own dumps. Bound receive work and schedule a
retry through the main loop even if no further notification arrives.
Startup makes at most three spaced attempts before reporting failure.

This is an RFC, rebased from the local 902 r2 candidate. The shared
iface_enumerate() helper is also used by DHCP, RA, ARP and lease discovery.
This changes its failure-return behaviour, including returning zero
instead of minus one on ENOBUFS. Those consumers are not made transactional
here; their behaviour under failed scans remains an open review and
runtime-validation item. Native kernel, OpenWrt/musl and protocol
acceptance are not claimed.

The current upstream TCP-child listener cleanup is left unchanged.
No OpenWrt integration changes, diagnostics hooks or fault-injection code
are included.

Prerequisite: 902-1-shared-listener-gc-upstream.patch

Base-commit: a9880c595f052d63859d6ed8aa86a3a6b007bf20

---
 src/dnsmasq.c |  15 +-
 src/dnsmasq.h |   2 +
 src/netlink.c | 200 ++++++++++++++--
 src/network.c | 744 +++++++++++++++++++++++++++++++++++++++-------------------
 4 files changed, 685 insertions(+), 276 deletions(-)

diff --git a/src/dnsmasq.c b/src/dnsmasq.c
index 10d25c1fe0066866f79aeb5ec552a968175522f6..c5a81399e5565fda51103a0d19f6b319665856bf 100644
--- a/src/dnsmasq.c
+++ b/src/dnsmasq.c
@@ -373,7 +373,7 @@ int main (int argc, char **argv)
   if (option_bool(OPT_NOWILD) && option_bool(OPT_CLEVERBIND))
     die(_("cannot set --bind-interfaces and --bind-dynamic"), NULL, EC_BADCONF);
   
-  if (!enumerate_interfaces(1) || !enumerate_interfaces(0))
+  if (!enumerate_interfaces_startup())
     die(_("failed to find list of interfaces: %s"), NULL, EC_MISC);
 
 #ifdef HAVE_DHCP
@@ -1105,6 +1105,10 @@ int main (int argc, char **argv)
   while (1)
     {
       int timeout = fast_retry(now);
+      int interface_timeout = interface_retry_timeout();
+
+      if (interface_timeout != -1 && (timeout == -1 || timeout > interface_timeout))
+        timeout = interface_timeout;
       
       poll_reset();
       
@@ -1225,18 +1229,21 @@ int main (int argc, char **argv)
       /* prime. */
       enumerate_interfaces(1);
 
+      /* Retry an incomplete scan even if no new address event arrives. */
+      if (interface_retry_timeout() == 0)
+        newaddress(now);
+
       /* Check the interfaces to see if any have exited DAD state
 	 and if so, bind the address. */
-      if (is_dad_listeners())
+      if (is_dad_listeners() && enumerate_interfaces(0))
 	{
-	  enumerate_interfaces(0);
 	  /* NB, is_dad_listeners() == 1 --> we're binding interfaces */
 	  create_bound_listeners(0);
 	  warn_bound_listeners();
 	}
 
 #if defined(HAVE_LINUX_NETWORK)
-      if (poll_check(daemon->netlinkfd, POLLIN))
+      if (poll_check(daemon->netlinkfd, POLLIN | POLLERR))
 	netlink_multicast();
 #elif defined(HAVE_BSD_NETWORK)
       if (poll_check(daemon->routefd, POLLIN))
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index ecff18d652573749cda46a90486300b0d8d83bbf..63718ab821fc5939c65a0fa2abf0afc2c5a1820d 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -1602,6 +1602,8 @@ void pre_allocate_sfds(void);
 int reload_servers(char *fname);
 void check_servers(int no_loop_call);
 int enumerate_interfaces(int reset);
+int enumerate_interfaces_startup(void);
+int interface_retry_timeout(void);
 void create_wildcard_listeners(void);
 void create_bound_listeners(int dienow);
 void warn_bound_listeners(void);
diff --git a/src/netlink.c b/src/netlink.c
index 385d4200c742207ab92253eb92b4d1e506c7264e..962af637e1287da6afac0a8060ff411ee685d0e1 100644
--- a/src/netlink.c
+++ b/src/netlink.c
@@ -54,6 +54,12 @@ enum async_states {
 static struct iovec iov;
 static u32 netlink_pid;
 
+/* Bound receive work in the single-threaded event loop. The dump budget is
+   per family, including interleaved notifications; failed snapshots retry
+   later. The multicast budget counts datagrams, not nlmsghdr records. */
+#define NETLINK_DUMP_TIMEOUT 1000
+#define NETLINK_MULTICAST_BATCH 64
+
 static unsigned nl_async(struct nlmsghdr *h, unsigned state);
 static void nl_multicast_state(unsigned state);
 
@@ -113,6 +119,11 @@ static ssize_t netlink_recv(int flags)
       
       while ((rc = recvmsg(daemon->netlinkfd, &msg, flags | MSG_PEEK | MSG_TRUNC)) == -1 &&
 	     errno == EINTR);
+
+      /* In particular, do not consume ENOBUFS with the peek and then treat
+         the following datagram as evidence of a complete dump. */
+      if (rc == -1)
+        return -1;
       
       /* make buffer big enough */
       if (rc != -1 && (msg.msg_flags & MSG_TRUNC))
@@ -149,7 +160,8 @@ static ssize_t netlink_recv(int flags)
 
 /* family = AF_UNSPEC finds ARP table entries.
    family = AF_LOCAL finds MAC addresses.
-   returns 0 on failure, 1 on success, -1 when restart is required
+   returns 0 on failure (including an interrupted dump), 1 on success.
+   Callers must discard partial callback results after a failure.
 */
 int iface_enumerate(int family, void *parm, callback_t callback)
 {
@@ -157,7 +169,8 @@ int iface_enumerate(int family, void *parm, callback_t callback)
   struct nlmsghdr *h;
   ssize_t len;
   static unsigned int seq = 0;
-  int callback_ok = 1;
+  int callback_ok = 1, errsave = 0, seen = 0;
+  u32 started;
   unsigned state = 0;
 
   struct {
@@ -196,34 +209,134 @@ int iface_enumerate(int family, void *parm, callback_t callback)
 
   if (errno != 0)
     return 0;
+
+  started = dnsmasq_milliseconds();
     
   while (1)
     {
-      if ((len = netlink_recv(0)) == -1)
+      struct pollfd pfd;
+      u32 elapsed = dnsmasq_milliseconds() - started;
+      int ready;
+
+      /* A lost terminator or persistent churn must not stall the main loop.
+         Nonblocking receive also closes the race between poll and recvmsg. */
+      if (elapsed >= NETLINK_DUMP_TIMEOUT)
 	{
-	  if (errno == ENOBUFS)
-	    {
-	      nl_multicast_state(state);
-	      return -1;
-	    }
+	  errno = ETIMEDOUT;
+	  return 0;
+	}
+      pfd.fd = daemon->netlinkfd;
+      pfd.events = POLLIN;
+      pfd.revents = 0;
+      ready = poll(&pfd, 1, NETLINK_DUMP_TIMEOUT - elapsed);
+      if (ready == -1 && errno == EINTR)
+        continue;
+      if (ready <= 0)
+        {
+          if (ready == 0)
+            errno = ETIMEDOUT;
+          return 0;
+        }
+
+      if ((len = netlink_recv(MSG_DONTWAIT)) == -1)
+	{
+	  if (errno == EAGAIN || errno == EWOULDBLOCK)
+            continue;
+	  if (errno == ENOBUFS && !(state & STATE_NEWADDR))
+            queue_event(EVENT_NEWADDR);
 	  return 0;
 	}
 
-      for (h = (struct nlmsghdr *)iov.iov_base; NLMSG_OK(h, (size_t)len); h = NLMSG_NEXT(h, len))
-	if (h->nlmsg_pid != netlink_pid || h->nlmsg_type == NLMSG_ERROR)
+      if (len == 0)
+        {
+          errno = EIO;
+          return 0;
+        }
+
+      for (h = (struct nlmsghdr *)iov.iov_base;
+           len >= (ssize_t)sizeof(*h) && NLMSG_OK(h, (size_t)len);
+           h = NLMSG_NEXT(h, len))
+        {
+	if (h->nlmsg_pid != netlink_pid &&
+            !(h->nlmsg_type == NLMSG_ERROR && h->nlmsg_seq == seq))
 	  {
 	    /* May be multicast arriving async */
 	    state = nl_async(h, state);
+            continue;
 	  }
-	else if (h->nlmsg_seq != seq)
+	if (h->nlmsg_seq != seq)
 	  {
 	    /* May be part of incomplete response to previous request after
 	       ENOBUFS. Drop it. */
 	    continue;
 	  }
-	else if (h->nlmsg_type == NLMSG_DONE)
-	  return callback_ok;
-	else if (h->nlmsg_type == RTM_NEWADDR && family != AF_UNSPEC && family != AF_LOCAL)
+        /* The flag can be on any message, including DONE or an ACK.
+           Drain this request, but stop callbacks once it is suspect. */
+        if (h->nlmsg_flags & NLM_F_DUMP_INTR)
+          {
+            callback_ok = 0;
+            errsave = EAGAIN;
+          }
+
+        if (h->nlmsg_type == NLMSG_ERROR)
+          {
+            struct nlmsgerr *err;
+            if (h->nlmsg_len < NLMSG_LENGTH(sizeof(*err)))
+              {
+                errno = EBADMSG;
+                return 0;
+              }
+            err = NLMSG_DATA(h);
+            if (err->error)
+              {
+                /* A kernel without IPv6 may reject the family outright.
+                   This is an empty IPv6 set, only before any dump data. */
+                if (family == AF_INET6 && !seen && callback_ok &&
+                    (err->error == -EAFNOSUPPORT || err->error == -EPROTONOSUPPORT))
+                  {
+                    errno = 0;
+                    return 1;
+                  }
+                errno = err->error < 0 ? -err->error : EIO;
+                return 0;
+              }
+            continue; /* zero-error ACK is not the dump terminator */
+          }
+
+        if (h->nlmsg_type == NLMSG_DONE)
+          {
+            /* New kernels supply a dump error in the DONE payload;
+               older kernels may supply no payload at all. */
+            if (h->nlmsg_len > NLMSG_LENGTH(0))
+              {
+                int error;
+                if (h->nlmsg_len < NLMSG_LENGTH(sizeof(error)))
+                  {
+                    errno = EBADMSG;
+                    return 0;
+                  }
+                memcpy(&error, NLMSG_DATA(h), sizeof(error));
+                if (error)
+                  {
+                    errno = error < 0 ? -error : EIO;
+                    return 0;
+                  }
+              }
+            errno = errsave;
+            return callback_ok;
+          }
+
+        if (h->nlmsg_type == NLMSG_OVERRUN)
+          {
+            errno = ENOBUFS;
+            return 0;
+          }
+
+        seen = 1;
+        if (!callback_ok)
+          continue;
+
+	if (h->nlmsg_type == RTM_NEWADDR && family != AF_UNSPEC && family != AF_LOCAL)
 	  {
 	    struct ifaddrmsg *ifa = NLMSG_DATA(h);  
 	    struct rtattr *rta = IFA_RTA(ifa);
@@ -255,7 +368,10 @@ int iface_enumerate(int family, void *parm, callback_t callback)
 		    
 		    if (addr.s_addr && callback_ok)
 		      if (!callback.af_inet(addr, ifa->ifa_index, label,  netmask, broadcast, parm))
-			callback_ok = 0;
+                        {
+                          callback_ok = 0;
+                          errsave = errno ? errno : EIO;
+                        }
 		  }
 		else if (ifa->ifa_family == AF_INET6)
 		  {
@@ -298,7 +414,10 @@ int iface_enumerate(int family, void *parm, callback_t callback)
 		      if (!callback.af_inet6(addrp, (int)(ifa->ifa_prefixlen), (int)(ifa->ifa_scope), 
 					(int)(ifa->ifa_index), flags, 
 					(unsigned int)preferred, (unsigned int)valid, parm))
-			callback_ok = 0;
+                        {
+                          callback_ok = 0;
+                          errsave = errno ? errno : EIO;
+                        }
 		  }
 	      }
 	  }
@@ -326,7 +445,10 @@ int iface_enumerate(int family, void *parm, callback_t callback)
 	    if (!(neigh->ndm_state & (NUD_NOARP | NUD_INCOMPLETE | NUD_FAILED)) &&
 		inaddr && mac && callback_ok)
 	      if (!callback.af_unspec(neigh->ndm_family, inaddr, mac, maclen, parm))
-		callback_ok = 0;
+                {
+                  callback_ok = 0;
+                  errsave = errno ? errno : EIO;
+                }
 	  }
 #ifdef HAVE_DHCP6
 	else if (h->nlmsg_type == RTM_NEWLINK && family == AF_LOCAL)
@@ -350,9 +472,23 @@ int iface_enumerate(int family, void *parm, callback_t callback)
 
 	    if (mac && callback_ok && !((link->ifi_flags & (IFF_LOOPBACK | IFF_POINTOPOINT))) && 
 		!callback.af_local((int)link->ifi_index, (unsigned int)link->ifi_type, mac, maclen, parm))
-	      callback_ok = 0;
+          {
+            callback_ok = 0;
+            errsave = errno ? errno : EIO;
+          }
 	  }
 #endif
+          }
+
+      /* NLMSG_NEXT subtracts the aligned length: a complete final message
+         without its optional padding can leave -1..-3 bytes. Never cast a
+         negative remainder to size_t in NLMSG_OK. Positive unparsed bytes
+         are not evidence of a complete datagram. */
+      if (len > 0 || len <= -(ssize_t)NLMSG_ALIGNTO)
+        {
+          errno = EBADMSG;
+          return 0;
+        }
     }
 }
 
@@ -360,14 +496,24 @@ static void nl_multicast_state(unsigned state)
 {
   ssize_t len;
   struct nlmsghdr *h;
+  unsigned int count;
 
-  do {
-    /* don't risk blocking reading netlink messages here. */
-    while ((len = netlink_recv(MSG_DONTWAIT)) != -1)
-  
-      for (h = (struct nlmsghdr *)iov.iov_base; NLMSG_OK(h, (size_t)len); h = NLMSG_NEXT(h, len))
+  /* Limit work per poll cycle, including repeated ENOBUFS. Remaining messages
+     keep the fd readable. Lost notifications require address reconciliation. */
+  for (count = 0; count < NETLINK_MULTICAST_BATCH; count++)
+    {
+      len = netlink_recv(MSG_DONTWAIT);
+      if (len <= 0)
+        {
+          if (len == -1 && (errno == ENOBUFS || errno == ENOMEM) && !(state & STATE_NEWADDR))
+            queue_event(EVENT_NEWADDR);
+          break;
+        }
+      for (h = (struct nlmsghdr *)iov.iov_base;
+           len >= (ssize_t)sizeof(*h) && NLMSG_OK(h, (size_t)len);
+           h = NLMSG_NEXT(h, len))
 	state = nl_async(h, state);
-  } while (errno == ENOBUFS);
+    }
 }
 
 void netlink_multicast(void)
@@ -403,7 +549,11 @@ static unsigned nl_async(struct nlmsghdr *h, unsigned state)
 	  state |= STATE_NEWROUTE;
 	}
     }
-  else if ((h->nlmsg_type == RTM_NEWADDR || h->nlmsg_type == RTM_DELADDR) &&
+  /* Kernel origin is checked using recvmsg's sockaddr_nl in netlink_recv().
+     Address notifications can carry the initiating process's port ID in
+     nlmsg_pid. Only discard our own outstanding dump replies here. */
+  else if (h->nlmsg_pid != netlink_pid &&
+           (h->nlmsg_type == RTM_NEWADDR || h->nlmsg_type == RTM_DELADDR) &&
 	   (state & STATE_NEWADDR)==0)
     {
       queue_event(EVENT_NEWADDR);
diff --git a/src/network.c b/src/network.c
index aa3ef89e714169dd523c567ddd331d217afb9fce..688b77a347b672eddb056c71078cfc2303e3863e 100644
--- a/src/network.c
+++ b/src/network.c
@@ -109,7 +109,8 @@ int indextoname(int fd, int index, char *name)
 
 #endif
 
-int iface_check(int family, union all_addr *addr, char *name, int *auth)
+static int iface_check_names(int family, union all_addr *addr, char *name, int *auth,
+                             struct iname *if_names, struct iname *if_addrs)
 {
   struct iname *tmp;
   int ret = 1, match_addr = 0;
@@ -117,11 +118,11 @@ int iface_check(int family, union all_addr *addr, char *name, int *auth)
   /* Note: have to check all and not bail out early, so that we set the "used" flags.
      May be called with family == AF_LOCAL to check interface by name only. */
   
-  if (daemon->if_names || daemon->if_addrs)
+  if (if_names || if_addrs)
     {
       ret = 0;
 
-      for (tmp = daemon->if_names; tmp; tmp = tmp->next)
+      for (tmp = if_names; tmp; tmp = tmp->next)
 	if (tmp->name && wildcard_match(tmp->name, name))
 	  {
 	    tmp->flags |= INAME_USED;
@@ -129,7 +130,7 @@ int iface_check(int family, union all_addr *addr, char *name, int *auth)
 	  }
 	        
       if (addr)
-	for (tmp = daemon->if_addrs; tmp; tmp = tmp->next)
+	for (tmp = if_addrs; tmp; tmp = tmp->next)
 	  if (tmp->addr.sa.sa_family == family)
 	    {
 	      if (family == AF_INET &&
@@ -182,6 +183,11 @@ int iface_check(int family, union all_addr *addr, char *name, int *auth)
 }
 
 
+int iface_check(int family, union all_addr *addr, char *name, int *auth)
+{
+  return iface_check_names(family, addr, name, auth, daemon->if_names, daemon->if_addrs);
+}
+
 /* Fix for problem that the kernel sometimes reports the loopback interface as the
    arrival interface when a packet originates locally, even when sent to address of 
    an interface other than the loopback. Accept packet if it arrived via a loopback 
@@ -231,11 +237,326 @@ int label_exception(int index, int family, union all_addr *addr)
   return 0;
 }
 
+/* The live records and list heads are never changed by dump callbacks.
+   Existing irec and iname copies form a suffix; callbacks may prepend new
+   records. Names in the copied suffix are borrowed and must not be freed. */
+struct iface_addr_update {
+  struct addrlist **target, *head;
+  struct iface_addr_update *next;
+};
+
+struct iface_index_update {
+  struct server *server;
+  unsigned int index;
+  struct iface_index_update *next;
+};
+
 struct iface_param {
   struct addrlist *spare;
+  struct iface_addr_update *lists;
+  struct iface_index_update *indexes;
+  struct irec *interfaces, *old_interfaces;
+  struct iname *if_names, *old_if_names, *if_addrs;
   int fd;
 };
 
+static int enumeration_done, enumeration_result = 1, enumeration_pending;
+static u32 enumeration_retry_at;
+
+/* Coalesce address churn and bound repeat work after an incomplete scan.
+   Startup has no committed snapshot to serve, so allow three attempts before
+   reporting failure. Runtime retries remain scheduled by the main loop. */
+#define INTERFACE_RETRY_INTERVAL 1000
+#define INTERFACE_STARTUP_ATTEMPTS 3
+
+/* -1: no retry; 0: due; otherwise milliseconds until the retry.
+   Like fast_retry(), tolerate wrapping milliseconds and clock adjustments. */
+int interface_retry_timeout(void)
+{
+  int delay;
+
+  if (!enumeration_pending)
+    return -1;
+
+  delay = (int)(enumeration_retry_at - dnsmasq_milliseconds());
+  return delay > 0 && delay <= INTERFACE_RETRY_INTERVAL ? delay : 0;
+}
+
+static struct addrlist *iface_addr_alloc(struct iface_param *param)
+{
+  struct addrlist *al = param->spare;
+
+  if (al)
+    param->spare = al->next;
+  else
+    al = whine_malloc(sizeof(*al));
+
+  if (al)
+    memset(al, 0, sizeof(*al));
+  else
+    errno = ENOMEM;
+
+  return al;
+}
+
+static void iface_addr_recycle(struct iface_param *param, struct addrlist *al)
+{
+  while (al)
+    {
+      struct addrlist *next = al->next;
+      al->next = param->spare;
+      param->spare = al;
+      al = next;
+    }
+}
+
+/* Every head is registered before the first callback. */
+static struct addrlist **iface_addr_head(struct iface_param *param, struct addrlist **target)
+{
+  struct iface_addr_update *update;
+
+  for (update = param->lists; update; update = update->next)
+    if (update->target == target)
+      return &update->head;
+
+  errno = EINVAL; /* missing registration is an internal invariant failure */
+  return NULL;
+}
+
+static struct addrlist *iface_addr_add(struct iface_param *param, struct addrlist **target)
+{
+  struct addrlist **head = iface_addr_head(param, target), *al;
+
+  if (!head || !(al = iface_addr_alloc(param)))
+    return NULL;
+
+  al->next = *head;
+  *head = al;
+  return al;
+}
+
+static int iface_addr_prepare(struct iface_param *param, struct addrlist **target, int literals)
+{
+  struct iface_addr_update *update = whine_malloc(sizeof(*update));
+  struct addrlist *al, **tail;
+
+  if (!update)
+    return 0;
+
+  update->target = target;
+  update->head = NULL;
+  update->next = param->lists;
+  param->lists = update;
+  tail = &update->head;
+
+  /* Auth-zone address literals are immutable configuration, but next pointers
+     belong to the list, so copy the nodes as well. Preserve their order. */
+  if (literals)
+    for (al = *target; al; al = al->next)
+      if (al->flags & ADDRLIST_LITERAL)
+        {
+          struct addrlist *copy = iface_addr_alloc(param);
+          if (!copy)
+            return 0;
+          *copy = *al;
+          copy->next = NULL;
+          *tail = copy;
+          tail = &copy->next;
+        }
+
+  return 1;
+}
+
+static int iface_copy_names(struct iname *src, struct iname **tail)
+{
+  for (; src; src = src->next)
+    {
+      struct iname *copy = whine_malloc(sizeof(*copy));
+      if (!copy)
+        return 0;
+      *copy = *src;
+      copy->next = NULL;
+      *tail = copy;
+      tail = &copy->next;
+    }
+  return 1;
+}
+
+static int iface_snapshot_prepare(struct iface_param *param)
+{
+  struct irec *iface, **tail = &param->interfaces;
+  struct interface_name *intname;
+  struct cond_domain *cond;
+  struct server *serv;
+  int ret;
+#ifdef HAVE_AUTH
+  struct auth_zone *zone;
+#endif
+
+  for (iface = daemon->interfaces; iface; iface = iface->next)
+    {
+      struct irec *copy = whine_malloc(sizeof(*copy));
+      if (!copy)
+        return 0;
+      *copy = *iface;
+      copy->found = 0;
+      copy->next = NULL;
+      *tail = copy;
+      tail = &copy->next;
+      /* Set this before the next allocation can fail. Otherwise rollback
+         would treat a partial clone as new records and free borrowed names. */
+      param->old_interfaces = param->interfaces;
+    }
+
+  ret = iface_copy_names(daemon->if_names, &param->if_names);
+  param->old_if_names = param->if_names;
+  if (!ret || !iface_copy_names(daemon->if_addrs, &param->if_addrs))
+    return 0;
+
+  if (!iface_addr_prepare(param, &daemon->interface_addrs, 0))
+    return 0;
+  for (intname = daemon->int_names; intname; intname = intname->next)
+    if (!iface_addr_prepare(param, &intname->addr, 0))
+      return 0;
+  for (cond = daemon->cond_domain; cond; cond = cond->next)
+    if (!iface_addr_prepare(param, &cond->al, 0))
+      return 0;
+#ifdef HAVE_AUTH
+  for (zone = daemon->auth_zones; zone; zone = zone->next)
+    if (zone->interface_names && !iface_addr_prepare(param, &zone->subnet, 1))
+      return 0;
+#endif
+
+  /* Stage server interface indexes too. A missing interface has index zero;
+     other ioctl errors invalidate the candidate, just like address errors. */
+  for (serv = daemon->servers; serv; serv = serv->next)
+    if (serv->interface[0])
+      {
+        struct iface_index_update *update = whine_malloc(sizeof(*update));
+        if (!update)
+          return 0;
+        update->server = serv;
+        update->index = 0;
+        update->next = param->indexes;
+        param->indexes = update;
+#ifdef HAVE_LINUX_NETWORK
+        {
+          struct ifreq ifr;
+          safe_strncpy(ifr.ifr_name, serv->interface, IF_NAMESIZE);
+          if (ioctl(param->fd, SIOCGIFINDEX, &ifr) != -1)
+            update->index = ifr.ifr_ifindex;
+          else if (errno != ENODEV && errno != ENXIO && errno != ENOENT)
+            return 0;
+        }
+#else
+        update->index = if_nametoindex(serv->interface);
+#endif
+      }
+
+  return 1;
+}
+
+static void iface_snapshot_finish(struct iface_param *param, int commit)
+{
+  struct irec **tail = &param->interfaces, *copy, *live;
+  struct iname **ntail = &param->if_names, *nc, *nl;
+
+  /* Keep the addresses of committed irec objects: listener->iface and other
+     users may point at them. Only these three fields changed on old records. */
+  while (*tail != param->old_interfaces)
+    if (commit)
+      tail = &(*tail)->next;
+    else
+      {
+        copy = *tail;
+        *tail = copy->next;
+        free(copy->name);
+        free(copy);
+      }
+
+  for (copy = param->old_interfaces, live = daemon->interfaces; copy; )
+    {
+      struct irec *next = copy->next;
+      if (commit)
+        {
+          live->found = copy->found;
+          live->dad = copy->dad;
+          live->netmask = copy->netmask;
+          live = live->next;
+        }
+      free(copy);
+      copy = next;
+    }
+  if (commit)
+    {
+      *tail = daemon->interfaces;
+      daemon->interfaces = param->interfaces;
+    }
+
+  /* The automatic loopback name is the only possible new iname. */
+  while (*ntail != param->old_if_names)
+    if (commit)
+      ntail = &(*ntail)->next;
+    else
+      {
+        nc = *ntail;
+        *ntail = nc->next;
+        free(nc->name);
+        free(nc);
+      }
+  for (nc = param->old_if_names, nl = daemon->if_names; nc; )
+    {
+      struct iname *next = nc->next;
+      if (commit)
+        {
+          nl->flags = nc->flags;
+          nl = nl->next;
+        }
+      free(nc);
+      nc = next;
+    }
+  if (commit)
+    {
+      *ntail = daemon->if_names;
+      daemon->if_names = param->if_names;
+    }
+
+  for (nc = param->if_addrs, nl = daemon->if_addrs; nc; )
+    {
+      struct iname *next = nc->next;
+      if (commit)
+        {
+          nl->flags = nc->flags;
+          nl = nl->next;
+        }
+      free(nc);
+      nc = next;
+    }
+
+  while (param->lists)
+    {
+      struct iface_addr_update *update = param->lists;
+      struct addrlist *old = update->head;
+      if (commit)
+        {
+          old = *update->target;
+          *update->target = update->head;
+        }
+      iface_addr_recycle(param, old);
+      param->lists = update->next;
+      free(update);
+    }
+
+  while (param->indexes)
+    {
+      struct iface_index_update *update = param->indexes;
+      if (commit)
+        update->server->ifindex = update->index;
+      param->indexes = update->next;
+      free(update);
+    }
+}
+
 static int iface_allowed(struct iface_param *param, int if_index, char *label,
 			 union mysockaddr *addr, struct in_addr netmask, int prefixlen, int iface_flags) 
 {
@@ -248,6 +569,7 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
   int dhcp6_ok = 1;
   int auth_dns = 0;
   int is_label = 0;
+  union all_addr interface_addr;
 #if defined(HAVE_DHCP) || defined(HAVE_TFTP)
   struct iname *tmp;
 #endif
@@ -273,31 +595,20 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
     {
       struct addrlist *al;
 
-      if (param->spare)
-	{
-	  al = param->spare;
-	  param->spare = al->next;
-	}
+      if (!(al = iface_addr_add(param, &daemon->interface_addrs)))
+        return 0;
+
+      al->prefixlen = prefixlen;
+      if (addr->sa.sa_family == AF_INET)
+        {
+          al->addr.addr4 = addr->in.sin_addr;
+          al->flags = 0;
+        }
       else
-	al = whine_malloc(sizeof(struct addrlist));
-      
-      if (al)
-	{
-	  al->next = daemon->interface_addrs;
-	  daemon->interface_addrs = al;
-	  al->prefixlen = prefixlen;
-	  
-	  if (addr->sa.sa_family == AF_INET)
-	    {
-	      al->addr.addr4 = addr->in.sin_addr;
-	      al->flags = 0;
-	    }
-	  else
-	    {
-	      al->addr.addr6 = addr->in6.sin6_addr;
-	      al->flags = ADDRLIST_IPV6;
-	    } 
-	}
+        {
+          al->addr.addr6 = addr->in6.sin6_addr;
+          al->flags = ADDRLIST_IPV6;
+        }
     }
   
   if (addr->sa.sa_family != AF_INET6 || !IN6_IS_ADDR_LINKLOCAL(&addr->in6.sin6_addr))
@@ -315,42 +626,22 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
 	    {
 	      if (addr->sa.sa_family == AF_INET && (name->flags & AUTH4))
 		{
-		  if (param->spare)
-		    {
-		      al = param->spare;
-		      param->spare = al->next;
-		    }
-		  else
-		    al = whine_malloc(sizeof(struct addrlist));
-		  
-		  if (al)
-		    {
-		      al->next = zone->subnet;
-		      zone->subnet = al;
-		      al->prefixlen = prefixlen;
-		      al->addr.addr4 = addr->in.sin_addr;
-		      al->flags = 0;
-		    }
+		  if (!(al = iface_addr_add(param, &zone->subnet)))
+		    return 0;
+
+		  al->prefixlen = prefixlen;
+		  al->addr.addr4 = addr->in.sin_addr;
+		  al->flags = 0;
 		}
 	      
 	      if (addr->sa.sa_family == AF_INET6 && (name->flags & AUTH6))
 		{
-		  if (param->spare)
-		    {
-		      al = param->spare;
-		      param->spare = al->next;
-		    }
-		  else
-		    al = whine_malloc(sizeof(struct addrlist));
-		  
-		  if (al)
-		    {
-		      al->next = zone->subnet;
-		      zone->subnet = al;
-		      al->prefixlen = prefixlen;
-		      al->addr.addr6 = addr->in6.sin6_addr;
-		      al->flags = ADDRLIST_IPV6;
-		    }
+		  if (!(al = iface_addr_add(param, &zone->subnet)))
+		    return 0;
+
+		  al->prefixlen = prefixlen;
+		  al->addr.addr6 = addr->in6.sin6_addr;
+		  al->flags = ADDRLIST_IPV6;
 		} 
 	    }
 #endif
@@ -360,7 +651,10 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
       for (int_name = daemon->int_names; int_name; int_name = int_name->next)
 	if (strncmp(label, int_name->intr, IF_NAMESIZE) == 0)
 	  {
-	    struct addrlist *lp;
+	    struct addrlist *lp, **head = iface_addr_head(param, &int_name->addr);
+
+	    if (!head)
+	      return 0;
 
 	    al = NULL;
 	    
@@ -373,25 +667,17 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
 		    (int_name->proto4.s_addr & ~netmask.s_addr);
 		
 		/* check for duplicates. */
-		for (lp = int_name->addr; lp; lp = lp->next)
+		for (lp = *head; lp; lp = lp->next)
 		  if (lp->flags == 0 && lp->addr.addr4.s_addr == newaddr.s_addr)
 		    break;
 		
 		if (!lp)
 		  {
-		    if (param->spare)
-		      {
-			al = param->spare;
-			param->spare = al->next;
-		      }
-		    else
-		      al = whine_malloc(sizeof(struct addrlist));
+		    if (!(al = iface_addr_alloc(param)))
+		      return 0;
 
-		    if (al)
-		      {
-			al->flags = 0;
-			al->addr.addr4 = newaddr;
-		      }
+		    al->flags = 0;
+		    al->addr.addr4 = newaddr;
 		  }
 	      }
 
@@ -420,38 +706,30 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
 		  }
 		
 		/* check for duplicates. */
-		for (lp = int_name->addr; lp; lp = lp->next)
+		for (lp = *head; lp; lp = lp->next)
 		  if ((lp->flags & ADDRLIST_IPV6) &&
 		      IN6_ARE_ADDR_EQUAL(&lp->addr.addr6, &newaddr))
 		    break;
 					
 		if (!lp)
 		  {
-		    if (param->spare)
-		      {
-			al = param->spare;
-			param->spare = al->next;
-		      }
-		    else
-		      al = whine_malloc(sizeof(struct addrlist));
+		    if (!(al = iface_addr_alloc(param)))
+		      return 0;
 		    
-		    if (al)
-		      {
-			al->flags = ADDRLIST_IPV6;
-			al->addr.addr6 = newaddr;
+		    al->flags = ADDRLIST_IPV6;
+		    al->addr.addr6 = newaddr;
 
-			/* Privacy addresses and addresses still undergoing DAD and deprecated addresses
-			   don't appear in forward queries, but will in reverse ones. */
-			if (!(iface_flags & IFACE_PERMANENT) || (iface_flags & (IFACE_DEPRECATED | IFACE_TENTATIVE)))
-			  al->flags |= ADDRLIST_REVONLY;
-		      }
+		    /* Privacy addresses and addresses still undergoing DAD and deprecated addresses
+		       don't appear in forward queries, but will in reverse ones. */
+		    if (!(iface_flags & IFACE_PERMANENT) || (iface_flags & (IFACE_DEPRECATED | IFACE_TENTATIVE)))
+		      al->flags |= ADDRLIST_REVONLY;
 		  }
 	      }
 	    
 	    if (al)
 	      {
-		al->next = int_name->addr;
-		int_name->addr = al;
+		al->next = *head;
+		*head = al;
 	      }
 	  }
     }
@@ -462,13 +740,8 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
       {
 	struct addrlist *al;
 
-	if (param->spare)
-	  {
-	    al = param->spare;
-	    param->spare = al->next;
-	  }
-	else
-	  al = whine_malloc(sizeof(struct addrlist));
+	if (!(al = iface_addr_add(param, &cond->al)))
+	  return 0;
 
 	if (addr->sa.sa_family == AF_INET)
 	  {
@@ -482,13 +755,11 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
 	  }
 
 	al->prefixlen = prefixlen;
-	al->next = cond->al;
-	cond->al = al;
       }
   
   /* check whether the interface IP has been added already 
      we call this routine multiple times. */
-  for (iface = daemon->interfaces; iface; iface = iface->next) 
+  for (iface = param->interfaces; iface; iface = iface->next)
     if (sockaddr_isequal(&iface->addr, addr) && iface->index == if_index)
       {
 	iface->dad = !!(iface_flags & IFACE_TENTATIVE);
@@ -499,33 +770,38 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
 
  /* If we are restricting the set of interfaces to use, make
      sure that loopback interfaces are in that set. */
-  if (daemon->if_names && loopback)
+  if (param->if_names && loopback)
     {
       struct iname *lo;
-      for (lo = daemon->if_names; lo; lo = lo->next)
+      for (lo = param->if_names; lo; lo = lo->next)
 	if (lo->name && strcmp(lo->name, ifr.ifr_name) == 0)
 	  break;
       
-      if (!lo && (lo = whine_malloc(sizeof(struct iname)))) 
-	{
-	  if ((lo->name = whine_malloc(strlen(ifr.ifr_name)+1)))
-	    {
-	      strcpy(lo->name, ifr.ifr_name);
-	      lo->flags |= INAME_USED;
-	      lo->next = daemon->if_names;
-	      daemon->if_names = lo;
-	    }
-	  else
-	    free(lo);
-	}
+      if (!lo)
+        {
+          if (!(lo = whine_malloc(sizeof(struct iname))))
+            return 0;
+          memset(lo, 0, sizeof(*lo));
+          if (!(lo->name = whine_malloc(strlen(ifr.ifr_name)+1)))
+            {
+              free(lo);
+              return 0;
+            }
+          strcpy(lo->name, ifr.ifr_name);
+          lo->flags = INAME_USED;
+          lo->next = param->if_names;
+          param->if_names = lo;
+        }
     }
   
-  if (addr->sa.sa_family == AF_INET &&
-      !iface_check(AF_INET, (union all_addr *)&addr->in.sin_addr, label, &auth_dns))
-    return 1;
+  /* A sockaddr's IPv4 address need not have union all_addr alignment. */
+  if (addr->sa.sa_family == AF_INET)
+    interface_addr.addr4 = addr->in.sin_addr;
+  else
+    interface_addr.addr6 = addr->in6.sin6_addr;
 
-  if (addr->sa.sa_family == AF_INET6 &&
-      !iface_check(AF_INET6, (union all_addr *)&addr->in6.sin6_addr, label, &auth_dns))
+  if (!iface_check_names(addr->sa.sa_family, &interface_addr, label, &auth_dns,
+                        param->if_names, param->if_addrs))
     return 1;
     
 #ifdef HAVE_DHCP
@@ -565,7 +841,12 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
       int mtu = 0;
 
       if (ioctl(param->fd, SIOCGIFMTU, &ifr) != -1)
-	mtu = ifr.ifr_mtu;
+        mtu = ifr.ifr_mtu;
+      else if (errno == ENODEV || errno == ENXIO)
+        {
+          free(iface);
+          return 0;
+        }
 
       iface->addr = *addr;
       iface->netmask = netmask;
@@ -582,8 +863,8 @@ static int iface_allowed(struct iface_param *param, int if_index, char *label,
       if ((iface->name = whine_malloc(strlen(ifr.ifr_name)+1)))
 	{
 	  strcpy(iface->name, ifr.ifr_name);
-	  iface->next = daemon->interfaces;
-	  daemon->interfaces = iface;
+	  iface->next = param->interfaces;
+	  param->interfaces = iface;
 	  return 1;
 	}
       free(iface);
@@ -721,137 +1002,66 @@ static int release_listener(struct listener *l)
 
 int enumerate_interfaces(int reset)
 {
-  static struct addrlist *spare = NULL;
-  static int done = 0;
+  static struct addrlist *spare;
+  static u32 last_warning;
+  static int warned;
   struct iface_param param;
-  int errsave, ret = 1;
-  struct addrlist *addr, *tmp;
-  struct interface_name *intname;
-  struct cond_domain *cond;
-  struct irec *iface;
-#ifdef HAVE_AUTH
-  struct auth_zone *zone;
-#endif
-  struct server *serv;
-  
-  /* Do this max once per select cycle  - also inhibits netlink socket use
-   in TCP child processes. */
+  int errsave, ret = 0;
 
+  /* Cache the result, not merely the fact that an attempt was made. TCP
+     children retain their last committed snapshot and never use netlink. */
   if (reset)
     {
-      done = 0;
+      enumeration_done = 0;
       return 1;
     }
+  if (enumeration_done || daemon->pipe_to_parent != -1)
+    return enumeration_result;
+  enumeration_done = 1;
 
-  if (done)
-    return 1;
-
-  done = 1;
+  if (interface_retry_timeout() > 0)
+    return enumeration_result = 0;
 
-  if ((param.fd = socket(PF_INET, SOCK_DGRAM, 0)) == -1)
-    return 0;
-
-  /* iface indexes can change when interfaces are created/destroyed. 
-     We use them in the main forwarding control path, when the path
-     to a server is specified by an interface, so cache them.
-     Update the cache here. */
-  for (serv = daemon->servers; serv; serv = serv->next)
-    if (serv->interface[0] != 0)
-      {
-#ifdef HAVE_LINUX_NETWORK
-	struct ifreq ifr;
-	
-	safe_strncpy(ifr.ifr_name, serv->interface, IF_NAMESIZE);
-	if (ioctl(param.fd, SIOCGIFINDEX, &ifr) != -1) 
-	  serv->ifindex = ifr.ifr_ifindex;
-#else
-	serv->ifindex = if_nametoindex(serv->interface);
-#endif
-      }
-    
-again:
-  /* Mark interfaces for garbage collection */
-  for (iface = daemon->interfaces; iface; iface = iface->next) 
-    iface->found = 0;
+  memset(&param, 0, sizeof(param));
+  param.spare = spare;
+  if ((param.fd = socket(PF_INET, SOCK_DGRAM, 0)) != -1 &&
+      iface_snapshot_prepare(&param) &&
+      iface_enumerate(AF_INET6, &param, (callback_t){.af_inet6=iface_allowed_v6}) == 1 &&
+      iface_enumerate(AF_INET, &param, (callback_t){.af_inet=iface_allowed_v4}) == 1)
+    ret = 1;
+
+  errsave = errno ? errno : EIO;
+  if (param.fd != -1)
+    close(param.fd);
+
+  /* No allocation, ioctl, or event processing during commit. */
+  iface_snapshot_finish(&param, ret);
+  spare = param.spare;
+  enumeration_result = ret;
 
-  /* remove addresses stored against interface_names */
-  for (intname = daemon->int_names; intname; intname = intname->next)
+  if (!ret)
     {
-      for (addr = intname->addr; addr; addr = tmp)
-	{
-	  tmp = addr->next;
-	  addr->next = spare;
-	  spare = addr;
-	}
-      
-      intname->addr = NULL;
+      u32 now = dnsmasq_milliseconds();
+      enumeration_pending = 1;
+      enumeration_retry_at = now + INTERFACE_RETRY_INTERVAL;
+      if (!warned || (u32)(now - last_warning) >= 60000)
+        {
+          my_syslog(LOG_WARNING, _("interface scan failed: %s; retaining previous state, retrying"),
+                    strerror(errsave));
+          last_warning = now;
+          warned = 1;
+        }
+      errno = errsave;
+      return 0;
     }
 
-  /* remove addresses stored against cond-domains. */
-  for (cond = daemon->cond_domain; cond; cond = cond->next)
-    {
-      for (addr = cond->al; addr; addr = tmp)
-	{
-	  tmp = addr->next;
-	  addr->next = spare;
-	  spare = addr;
-      }
-      
-      cond->al = NULL;
-    }
-  
-  /* Remove list of addresses of local interfaces */
-  for (addr = daemon->interface_addrs; addr; addr = tmp)
-    {
-      tmp = addr->next;
-      addr->next = spare;
-      spare = addr;
-    }
-  daemon->interface_addrs = NULL;
-  
-#ifdef HAVE_AUTH
-  /* remove addresses stored against auth_zone subnets, but not 
-   ones configured as address literals */
-  for (zone = daemon->auth_zones; zone; zone = zone->next)
-    if (zone->interface_names)
-      {
-	struct addrlist **up;
-	for (up = &zone->subnet, addr = zone->subnet; addr; addr = tmp)
-	  {
-	    tmp = addr->next;
-	    if (addr->flags & ADDRLIST_LITERAL)
-	      up = &addr->next;
-	    else
-	      {
-		*up = addr->next;
-		addr->next = spare;
-		spare = addr;
-	      }
-	  }
-      }
-#endif
+  enumeration_pending = 0;
 
-  param.spare = spare;
-  
-  ret = iface_enumerate(AF_INET6, &param, (callback_t){.af_inet6=iface_allowed_v6});
-  if (ret < 0)
-    goto again;
-  else if (ret)
-    {
-      ret = iface_enumerate(AF_INET, &param, (callback_t){.af_inet=iface_allowed_v4});
-      if (ret < 0)
-	goto again;
-    }
- 
-  errsave = errno;
-  close(param.fd);
-  
   if (option_bool(OPT_CLEVERBIND))
     { 
       /* Garbage-collect listeners listening on addresses that no longer exist.
-	 Does nothing when not binding interfaces or for listeners on localhost, 
-	 since the ->iface field is NULL. Note that this needs the protections
-	 against reentrancy, hence it's here.  It also means there's a possibility,
+	 Listeners without an interface record are not collected here. This
+         needs the protection against reentrancy, hence it is here.  It also means there's a possibility,
 	 in OPT_CLEVERBIND mode, that at listener will just disappear after
 	 a call to enumerate_interfaces, this is checked OK on all calls. */
       struct listener *l, *tmp, **up;
@@ -876,10 +1086,32 @@ again:
 	clean_interfaces();
     }
 
-  errno = errsave;
-  spare = param.spare;
-  
-  return ret;
+  errno = 0;
+  return 1;
+}
+
+/* Called only before startup creates listeners. Each failed candidate is
+   discarded by enumerate_interfaces(); never start from an incomplete set. */
+int enumerate_interfaces_startup(void)
+{
+  int attempt, delay;
+
+  for (attempt = 0; attempt < INTERFACE_STARTUP_ATTEMPTS; attempt++)
+    {
+      enumerate_interfaces(1);
+      if (enumerate_interfaces(0))
+        return 1;
+      if (attempt + 1 == INTERFACE_STARTUP_ATTEMPTS)
+        break;
+
+      /* No event loop exists yet. Sleep until the scheduled retry; recompute
+         after EINTR so signals cannot turn this into a tight retry loop. */
+      while ((delay = interface_retry_timeout()) > 0)
+        if (poll(NULL, 0, delay) == -1 && errno != EINTR)
+          return 0;
+    }
+
+  return 0;
 }
 
 /* set NONBLOCK bit on fd: See Stevens 16.6 */
@@ -1183,6 +1415,10 @@ void create_bound_listeners(int dienow)
   struct iname *if_tmp;
   struct listener *existing;
 
+  /* All callers must reconcile only a successfully committed scan. */
+  if (!enumeration_result)
+    return;
+
   for (iface = daemon->interfaces; iface; iface = iface->next)
     if (!iface->done && !iface->dad && iface->found)
       {
@@ -1774,15 +2010,29 @@ int reload_servers(char *fname)
 /* Called when addresses are added or deleted from an interface */
 void newaddress(time_t now)
 {
+  int cached = enumeration_done;
+  int refresh = enumeration_pending || option_bool(OPT_CLEVERBIND) ||
+    option_bool(OPT_LOCAL_SERVICE) || daemon->doing_dhcp6 || daemon->relay6 || daemon->doing_ra;
 #ifdef HAVE_DHCP
   struct dhcp_relay *relay;
 #endif
   
   (void)now;
   
-  if (option_bool(OPT_CLEVERBIND) || option_bool(OPT_LOCAL_SERVICE) ||
-      daemon->doing_dhcp6 || daemon->relay6 || daemon->doing_ra)
-    enumerate_interfaces(0);
+  if (refresh && !enumerate_interfaces(0))
+    return;
+
+  /* The notification may describe a change after this poll cycle's scan.
+     Do not consume it merely by returning a cached success. */
+  if (refresh && cached)
+    {
+      if (!enumeration_pending)
+        {
+          enumeration_pending = 1;
+          enumeration_retry_at = dnsmasq_milliseconds() + INTERFACE_RETRY_INTERVAL;
+        }
+      return;
+    }
   
   if (option_bool(OPT_CLEVERBIND))
     create_bound_listeners(0);
