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

Simon Kelley simon at thekelleys.org.uk
Mon May 4 15:45:14 UTC 2026


Good catch.

That code has been there eating memory for 15 years, and was added just 
before the dnsmasq git repository started, so how it ended up like that 
is a bit of a mystery.

The code looks like it's using a common design pattern in dnsmasq, where 
the buffer is stored in a long-lived iovec and expand_buf() only does 
anything if the existing buffer is too small. This avoids lots of 
malloc()/free() calls in hot code paths and resulting heap 
fragmentation. The only problem is that the iovec in this case is not 
long-lived. My guess is that this code got copied from elsewhere, and 
the importance of that detail was missed.

The fix is to declare struct iovec buff as static. Then the buffer 
becomes long-lived and all the rest of the code, without any free() 
calls, makes sense.

I just committed

https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=commit;h=4bcc9650fac9a48502679cd793d269ef60caef07

which does exactly that. It would be great if you could check it works 
OK on your tests.


What would be _really_ nice (hint, hint) would be to extend this code to 
return IPv6 neighbours.


Cheers,

Simon.




On 26.04.2026 17:18, Sagie Duchovne-Nave 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;
>   }
> 
> _______________________________________________
> Dnsmasq-discuss mailing list
> Dnsmasq-discuss at lists.thekelleys.org.uk
> https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss
> 




More information about the Dnsmasq-discuss mailing list