[Dnsmasq-discuss] [PATCH] bpf.c: fix memory leak in arp_enumerate() on BSD

Sagie D. sagie.duchovne at gmail.com
Tue Apr 28 20:15:54 UTC 2026


I tried reapplying the previous patch on another folder and realized
the diff was malformed, so I'm attaching a fixed patch diff -- sorry
about that.
This fixed patch also adds NULL checks to avoid attempting to free an
unallocated buffer in edge cases.

Signed-off-by: Sagie D.
---
diff --git a/src/bpf.c b/src/bpf.c
index dd67735..e5bf6d9 100644
--- a/src/bpf.c
+++ b/src/bpf.c
@@ -77,14 +77,20 @@ int arp_enumerate(void *parm, callback_t callback)
   while (1)
     {
       if (!expand_buf(&buff, needed))
-       return 0;
+        {
+          if (buff.iov_base) free(buff.iov_base);
+          return 0;
+        }
       if ((rc = sysctl(mib, 6, buff.iov_base, &needed, NULL, 0)) == 0 ||
-         errno != ENOMEM)
-       break;
+          errno != ENOMEM)
+        break;
       needed += needed / 8;
     }
   if (rc == -1)
-    return 0;
+    {
+      if (buff.iov_base) free(buff.iov_base);
+      return 0;
+    }

   for (next = buff.iov_base ; next < (char *)buff.iov_base + needed;
next += rtm->rtm_msglen)
     {
@@ -92,9 +98,13 @@ int arp_enumerate(void *parm, callback_t callback)
       sin2 = (struct sockaddr_inarp *)(rtm + 1);
       sdl = (struct sockaddr_dl *)((char *)sin2 + SA_SIZE(sin2));
       if (!callback.af_unspec(AF_INET, &sin2->sin_addr, LLADDR(sdl),
sdl->sdl_alen, parm))
-       return 0;
+        {
+          if (buff.iov_base) free(buff.iov_base);
+          return 0;
+        }
     }

+  if (buff.iov_base) free(buff.iov_base);
   return 1;
 }
 #endif /* defined(HAVE_BSD_NETWORK) && !defined(__APPLE__) */


On Sun, 26 Apr 2026 at 19:18, Sagie Duchovne-Nave
<sagie.duchovne at gmail.com> wrote:
>
> arp_enumerate() allocates a heap buffer via expand_buf() to hold the
> kernel ARP table dump retrieved through sysctl(NET_RT_FLAGS). This
> buffer is never freed on any return path -- neither the early error
> returns nor the normal return after iteration -- causing a leak on
> every call.
>
> The leak is most acute in the DHCPv6 path. get_client_mac() calls
> find_mac() up to five times per packet with lazy=0. Because the
> 'updated' flag is local to each find_mac() invocation, a cached
> ARP_EMPTY entry for an unresolvable IPv6 address does not short-
> circuit the kernel lookup: each call falls through to iface_enumerate()
> -> arp_enumerate(), leaking one buffer per call. This yields up to
> five leaked allocations per DHCPv6 SOLICIT packet. The leak size per
> call equals the full system-wide IPv4 ARP table dump across all
> interfaces.
>
> The condition is readily triggered by a DHCPv6 client whose MAC
> address cannot be resolved via NDP -- which is the common case on
> FreeBSD, because arp_enumerate() queries NET_RT_FLAGS/RTF_LLINFO,
> which returns IPv4 ARP entries only; IPv6 NDP neighbour entries are
> not included. As a result every IPv6 MAC lookup fails unconditionally
> on FreeBSD, every failed lookup produces an ARP_EMPTY record that is
> never promoted, and every subsequent packet for that client leaks five
> buffers.
>
> Fix: free buff.iov_base on all return paths in arp_enumerate(),
> including the early returns inside the retry loop where iov_base may
> already be non-NULL from a prior expand_buf() call.
>
> Reported against: FreeBSD 14, dnsmasq 2.91
> Observed symptom: steady process memory growth correlated with DHCPv6
> SOLICIT traffic from a client whose NDP entry is absent from the
> kernel table (confirmed by disabling the client stopping the balloon).
>
> Signed-off-by: Sagie D.
> ---
>  bpf.c | 8 +++++++-
>  1 file changed, 7 insertions(+), 1 deletion(-)
>
> diff --git a/bpf.c b/bpf.c
> index XXXXXXX..XXXXXXX 100644
> --- a/bpf.c
> +++ b/bpf.c
> @@ -xx,12 +xx,18 @@ int arp_enumerate(void *parm, callback_t callback)
>    while (1)
>      {
>        if (!expand_buf(&buff, needed))
> -        return 0;
> +        {
> +          free(buff.iov_base);
> +          return 0;
> +        }
>        if ((rc = sysctl(mib, 6, buff.iov_base, &needed, NULL, 0)) == 0 ||
>            errno != ENOMEM)
>          break;
>        needed += needed / 8;
>      }
>    if (rc == -1)
> -    return 0;
> +    {
> +      free(buff.iov_base);
> +      return 0;
> +    }
>
>    for (next = buff.iov_base ; next < (char *)buff.iov_base + needed;
> next += rtm->rtm_msglen)
>      {
>        rtm = (struct rt_msghdr *)next;
>        sin2 = (struct sockaddr_inarp *)(rtm + 1);
>        sdl = (struct sockaddr_dl *)((char *)sin2 + SA_SIZE(sin2));
>        if (!callback.af_unspec(AF_INET, &sin2->sin_addr, LLADDR(sdl),
> sdl->sdl_alen, parm))
> -        return 0;
> +        {
> +          free(buff.iov_base);
> +          return 0;
> +        }
>      }
>
> -  return 1;
> +  free(buff.iov_base);
> +  return 1;
>  }



More information about the Dnsmasq-discuss mailing list