Re: MBuf leak in ether_output()

Taylor R Campbell <[email protected]>
Newsgroups gmane.os.netbsd.devel.network
Message-ID <[email protected]>
> Date: Fri, 19 Apr 2024 13:06:46 +0200
> From: Edgar Fuß <[email protected]>
> 
> I'm still chasing down an MBuf leak and it looks like I've found the culprit.
> 
> static int
> ether_output(struct ifnet * const ifp0, struct mbuf * const m0,
> 	const struct sockaddr * const dst,
> 	const struct rtentry *rt)
> {
> [...]
> 	case AF_INET6:
> [...]
> 		} else {
> 			error = nd6_resolve(ifp, rt, m, dst, edst,
> 			    sizeof(edst));
> 			if (error != 0)
> 				return error == EWOULDBLOCK ? 0 : error;
> 			}
> 		}
> [...]
> }
> 
> In the EWOULDBLOCK case, 0 is returned, but who frees m?

If nd6_resolve returns nonzero, that means it has taken responsibility
for m.  Either it has freed m synchronously, or it will put m on a
queue to be freed later.  At least, that's the contract; of course
there may be bugs in its implementation of the contract.

The only path where nd6_resolve could return EWOULDBLOCK is via
nd_resolve:

   1572 int
   1573 nd6_resolve(struct ifnet *ifp, const struct rtentry *rt, struct mbuf *m,
   1574     const struct sockaddr *_dst, uint8_t *lldst, size_t dstsize)
...
   1584 		m_freem(m);
   1585 		return ENETDOWN; /* better error? */
...
   1603 		return 0;
...
   1623 			m_freem(m);
   1624 			return ENOBUFS;
...
   1630 		m_freem(m);
   1631 		return ENETDOWN; /* better error? */
...
   1634 	error = nd_resolve(ln, rt, m, lldst, dstsize);
   1635 
   1636 	if (created)
   1637 		nd6_gc_neighbors(LLTABLE6(ifp), &dst->sin6_addr);
   1638 
   1639 	return error;

https://nxr.netbsd.org/xref/src/sys/netinet6/nd6.c?r=1.282#1572


If nd_resolve can't resolve the link-layer address for the next hop
immediately, it puts the packet on a queue of packets with the same
next hop address, to be retransmitted once the link-layer address has
been resolved, and returns EWOULDBLOCK (unless the queue has
overflowed, in which case it does m_freem on oldest packets first):

    305 int
    306 nd_resolve(struct llentry *ln, const struct rtentry *rt, struct mbuf *m,
    307     uint8_t *lldst, size_t dstsize)
...
    339 	/*
    340 	 * If the neighbor cache entry has a state other than INCOMPLETE
    341 	 * (i.e. its link-layer address is already resolved), just
    342 	 * send the packet.
    343 	 */
    344 	if (ln->ln_state > ND_LLINFO_INCOMPLETE) {
...
    348 		return 0;
    349 	}
...
    365 	if (ln->ln_hold != NULL) {
    366 		struct mbuf *m_hold;
    367 		int i;
    368 
    369 		i = 0;
    370 		for (m_hold = ln->ln_hold; m_hold; m_hold = m_hold->m_nextpkt) {
    371 			i++;
    372 			if (m_hold->m_nextpkt == NULL) {
    373 				m_hold->m_nextpkt = m;
    374 				break;
    375 			}
    376 		}
    377 		while (i >= nd->nd_maxqueuelen) {
    378 			m_hold = ln->ln_hold;
    379 			ln->ln_hold = ln->ln_hold->m_nextpkt;
    380 			m_freem(m_hold);
    381 			i--;
    382 		}
    383 	} else
    384 		ln->ln_hold = m;
...
    386 	if (ln->ln_asked >= nd->nd_mmaxtries)
    387 		error = (rt != NULL && rt->rt_flags & RTF_GATEWAY) ?
    388 		    EHOSTUNREACH : EHOSTDOWN;
    389 	else
    390 		error = EWOULDBLOCK;
...
    412 	return error;

https://nxr.netbsd.org/xref/src/sys/net/nd.c?r=1.5#305


There are generally two possible outcomes:

1. The link-layer address is resolved.

   When this happens, nd6_llinfo_release_pkts is supposed to be called
   to retransmit all the packets (note: la_hold and ln_hold are the
   same member, thanks to a confusing #define at
   <https://nxr.netbsd.org/xref/src/sys/net/if_llatbl.h?r=1.19#104>):

      1295 void
      1296 nd6_llinfo_release_pkts(struct llentry *ln, struct ifnet *ifp)
      1297 {
   ...
      1305 	m_hold = ln->la_hold, ln->la_hold = NULL, ln->la_numheld = 0;
   ...
      1309 	for (; m_hold != NULL; m_hold = m_hold_next) {
      1310 		m_hold_next = m_hold->m_nextpkt;
      1311 		m_hold->m_nextpkt = NULL;
      1312 
      1313 		/*
      1314 		 * we assume ifp is not a p2p here, so
      1315 		 * just set the 2nd argument as the
      1316 		 * 1st one.
      1317 		 */
      1318 		ip6_if_output(ifp, ifp, m_hold, &sin6, NULL);

   https://nxr.netbsd.org/xref/src/sys/netinet6/nd6.c?r=1.282#1295

   ip6_if_output is presumably responsible for freeing m_hold.

2. The link-layer address resolution times out.

   When this happens, nd_timer just frees all the queued packets but
   one, and calls nd6_llinfo_missed (or arp_llinfo_missed) for the
   oldest queued packet:

        99 			m = ln->ln_hold;
       100 			for (m0 = m->m_nextpkt; m0 != NULL; m0 = mnxt) {
       101 				mnxt = m0->m_nextpkt;
       102 				m0->m_nextpkt = NULL;
       103 				m_freem(m0);
       104 			}
       105 
       106 			m->m_nextpkt = NULL;
       107 			ln->ln_hold = NULL;
   ...
       206 	if (missed != ND_LLINFO_NOSTATE)
       207 		nd->nd_missed(ifp, &taddr, missed, m);

   https://nxr.netbsd.org/xref/src/sys/net/nd.c?r=1.5#99

   nd6_llinfo_missed, in turn, winds up either in m_freem or
   ip6_output via icmp6_reflect via icmp6_error via icmp6_error2, and
   ip6_output is presumably responsible for freeing it when done.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.