tech-net archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

Re: MBuf leak in ether_output()



> Date: Fri, 19 Apr 2024 13:06:46 +0200
> From: Edgar Fuß <ef%math.uni-bonn.de@localhost>
> 
> 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.


Home | Main Index | Thread Index | Old Index