⬢ DragonFlyBSD Kernel Audit
DF-5847 / fix5847.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""DF-5847 fix patcher -- serialize llinfo_nd6 list mutation vs the nd6
timer walk (nd6_mtx), tombstone+defer-free unlinked llinfo entries so a
concurrent walker holding `next` can never deref freed memory, and remove
the nd6_mtx-holds that the new lock scope would make recursive
(rt6_flush hoist; timer unlock windows around nd6_free / ia6 purge)."""
import sys

path = sys.argv[1] if len(sys.argv) > 1 else "nd6.c"
src = open(path).read()

subs = []

# ---- 1. deferred-free list head + drain helper (after nd6_mtx decl) ----
subs.append((
"""struct mtx nd6_mtx = MTX_INITIALIZER("nd6");
""",
"""struct mtx nd6_mtx = MTX_INITIALIZER("nd6");

/*
 * DF-5847: unlinked llinfo entries are tombstoned (ln_rt = NULL) and
 * moved onto this deferred-free list instead of being freed in place.
 * A concurrent nd6_timer walk (or ndp ioctl walk) may still hold a
 * pointer to the entry as its `next`; tombstoned entries keep their
 * ln_next chain intact so such walks continue correctly, and the memory
 * is only released by nd6_timer_dispatch() under nd6_mtx after its walk
 * has finished.
 */
static struct llinfo_nd6 *nd6_lnfreelist;
"""))

# ---- 2. nd6_free: hoist reentrant calls out of the nd6_mtx region ----
subs.append((
"""	if (!ip6_forwarding &&
	    (ND_IFINFO(rt->rt_ifp)->flags & ND6_IFF_ACCEPT_RTADV)) {
		mtx_lock(&nd6_mtx);
		dr = defrouter_lookup(
		    &((struct sockaddr_in6 *)rt_key(rt))->sin6_addr,
		    rt->rt_ifp);

		if (ln->ln_router || dr) {
			/*
			 * rt6_flush must be called whether or not the neighbor
			 * is in the Default Router List.
			 * See a corresponding comment in nd6_na_input().
			 */
			rt6_flush(&in6, rt->rt_ifp);
		}

		if (dr) {
			/*
			 * Unreachablity of a router might affect the default
			 * router selection and on-link detection of advertised
			 * prefixes.
			 */

			/*
			 * Temporarily fake the state to choose a new default
			 * router and to perform on-link determination of
			 * prefixes correctly.
			 * Below the state will be set correctly,
			 * or the entry itself will be deleted.
			 */
			ln->ln_state = ND6_LLINFO_INCOMPLETE;

			/*
			 * Since defrouter_select() does not affect the
			 * on-link determination and MIP6 needs the check
			 * before the default router selection, we perform
			 * the check now.
			 */
			pfxlist_onlink_check();

			if (dr == TAILQ_FIRST(&nd_defrouter)) {
				/*
				 * It is used as the current default router,
				 * so we have to move it to the end of the
				 * list and choose a new one.
				 * XXX: it is not very efficient if this is
				 *      the only router.
				 */
				TAILQ_REMOVE(&nd_defrouter, dr, dr_entry);
				TAILQ_INSERT_TAIL(&nd_defrouter, dr, dr_entry);

				defrouter_select();
			}
		}
		mtx_unlock(&nd6_mtx);
	}
""",
"""	int need_rt6_flush = 0;

	if (!ip6_forwarding &&
	    (ND_IFINFO(rt->rt_ifp)->flags & ND6_IFF_ACCEPT_RTADV)) {
		mtx_lock(&nd6_mtx);
		dr = defrouter_lookup(
		    &((struct sockaddr_in6 *)rt_key(rt))->sin6_addr,
		    rt->rt_ifp);

		if (ln->ln_router || dr) {
			/*
			 * rt6_flush must be called whether or not the neighbor
			 * is in the Default Router List.
			 * See a corresponding comment in nd6_na_input().
			 */
			need_rt6_flush = 1;
		}

		if (dr) {
			/*
			 * Unreachablity of a router might affect the default
			 * router selection and on-link detection of advertised
			 * prefixes.
			 */

			/*
			 * Temporarily fake the state to choose a new default
			 * router and to perform on-link determination of
			 * prefixes correctly.
			 * Below the state will be set correctly,
			 * or the entry itself will be deleted.
			 */
			ln->ln_state = ND6_LLINFO_INCOMPLETE;

			/*
			 * DF-5847: pfxlist_onlink_check() and
			 * defrouter_select() add/delete routes and reach
			 * nd6_rtrequest(), which now takes nd6_mtx -- run
			 * them with the mutex released (order otherwise as
			 * before: onlink check first, then router rotation).
			 */
			mtx_unlock(&nd6_mtx);
			pfxlist_onlink_check();
			mtx_lock(&nd6_mtx);

			if (dr == TAILQ_FIRST(&nd_defrouter)) {
				/*
				 * It is used as the current default router,
				 * so we have to move it to the end of the
				 * list and choose a new one.
				 * XXX: it is not very efficient if this is
				 *      the only router.
				 */
				TAILQ_REMOVE(&nd_defrouter, dr, dr_entry);
				TAILQ_INSERT_TAIL(&nd_defrouter, dr, dr_entry);
				mtx_unlock(&nd6_mtx);
				defrouter_select();
				mtx_lock(&nd6_mtx);
			}
		}
		mtx_unlock(&nd6_mtx);
	}

	if (need_rt6_flush)
		rt6_flush(&in6, rt->rt_ifp);
"""))

# ---- 3. RTM_ADD/RESOLVE: serialize alloc + linkage ----
subs.append((
"""		R_Malloc(ln, struct llinfo_nd6 *, sizeof(*ln));
		rt->rt_llinfo = (caddr_t)ln;
		if (!ln) {
			log(LOG_DEBUG, "nd6_rtrequest: malloc failed\\n");
			break;
		}
		nd6_inuse++;
		nd6_allocated++;
		bzero(ln, sizeof(*ln));
		ln->ln_rt = rt;""",
"""		R_Malloc(ln, struct llinfo_nd6 *, sizeof(*ln));
		if (!ln) {
			log(LOG_DEBUG, "nd6_rtrequest: malloc failed\\n");
			break;
		}
		/*
		 * DF-5847: the llinfo_nd6 list is walked by
		 * nd6_timer_dispatch() and nd6 ioctl handlers under
		 * nd6_mtx; serialize the publication of the new entry
		 * (ln_rt must be visible before the list linkage).
		 */
		mtx_lock(&nd6_mtx);
		rt->rt_llinfo = (caddr_t)ln;
		nd6_inuse++;
		nd6_allocated++;
		bzero(ln, sizeof(*ln));
		ln->ln_rt = rt;"""))
subs.append((
"""		rt->rt_flags |= RTF_LLINFO;
		ln->ln_next = llinfo_nd6.ln_next;
		llinfo_nd6.ln_next = ln;
		ln->ln_prev = &llinfo_nd6;
		ln->ln_next->ln_prev = ln;
""",
"""		rt->rt_flags |= RTF_LLINFO;
		ln->ln_next = llinfo_nd6.ln_next;
		llinfo_nd6.ln_next = ln;
		ln->ln_prev = &llinfo_nd6;
		ln->ln_next->ln_prev = ln;
		mtx_unlock(&nd6_mtx);
"""))

# ---- 4. RTM_DELETE: unlink + tombstone + defer free under nd6_mtx ----
subs.append((
"""		nd6_inuse--;
		ln->ln_next->ln_prev = ln->ln_prev;
		ln->ln_prev->ln_next = ln->ln_next;
		ln->ln_prev = NULL;
		rt->rt_llinfo = 0;
		rt->rt_flags &= ~RTF_LLINFO;
		if (ln->ln_hold)
			m_freem(ln->ln_hold);
		R_Free(ln);
	}
}""",
"""		if (ln->ln_hold) {
			m_freem(ln->ln_hold);
			ln->ln_hold = NULL;
		}
		/*
		 * DF-5847: unlink under nd6_mtx, then tombstone (ln_rt =
		 * NULL) and defer the free.  A concurrent timer/ioctl walk
		 * may still hold this entry as its saved `next`; the
		 * tombstone makes those walkers skip it (they check
		 * ln_rt != NULL), its ln_next stays valid so walks can
		 * continue past it, and nd6_timer_dispatch() releases the
		 * memory under nd6_mtx after its walk completes.
		 */
		mtx_lock(&nd6_mtx);
		nd6_inuse--;
		ln->ln_next->ln_prev = ln->ln_prev;
		ln->ln_prev->ln_next = ln->ln_next;
		ln->ln_prev = nd6_lnfreelist;
		nd6_lnfreelist = ln;
		ln->ln_rt = NULL;		/* tombstone */
		rt->rt_llinfo = 0;
		rt->rt_flags &= ~RTF_LLINFO;
		mtx_unlock(&nd6_mtx);
	}
}"""))

# ---- 5. timer: unlock around nd6_free (WAITDELETE + STALE via helper) ----
subs.append((
"""		switch (ln->ln_state) {
		case ND6_LLINFO_WAITDELETE:
			next = nd6_free(rt);
			break;""",
"""		switch (ln->ln_state) {
		case ND6_LLINFO_WAITDELETE:
			/*
			 * DF-5847: nd6_free() deletes the route and reaches
			 * nd6_rtrequest(), which now takes nd6_mtx; drop the
			 * mutex around it.  The saved `next` stays safe:
			 * concurrent deletes only tombstone entries.
			 */
			mtx_unlock(&nd6_mtx);
			next = nd6_free(rt);
			mtx_lock(&nd6_mtx);
			break;"""))
subs.append((
"""		case ND6_LLINFO_STALE:
			/* Garbage Collection(RFC 2461 5.3) */
			if (ln->ln_expire)
				next = nd6_free(rt);
			break;""",
"""		case ND6_LLINFO_STALE:
			/* Garbage Collection(RFC 2461 5.3) */
			if (ln->ln_expire) {
				mtx_unlock(&nd6_mtx);	/* DF-5847 */
				next = nd6_free(rt);
				mtx_lock(&nd6_mtx);
			}
			break;"""))
subs.append((
"""			} else {
				rt_rtmsg(RTM_MISS, rt, rt->rt_ifp, 0);
				next = nd6_free(rt);
			}
			break;""",
"""			} else {
				rt_rtmsg(RTM_MISS, rt, rt->rt_ifp, 0);
				mtx_unlock(&nd6_mtx);	/* DF-5847 */
				next = nd6_free(rt);
				mtx_lock(&nd6_mtx);
			}
			break;"""))

# ---- 6. timer: unlock around regen_tmpaddr/in6_purgeaddr (ia6 expiry) ----
subs.append((
"""			if (ip6_use_tempaddr &&
			    (ia6->ia6_flags & IN6_IFF_TEMPORARY)) {
				if (regen_tmpaddr(ia6) == 0)
					regen = 1;
			}

			in6_purgeaddr(&ia6->ia_ifa);
""",
"""			if (ip6_use_tempaddr &&
			    (ia6->ia6_flags & IN6_IFF_TEMPORARY)) {
				mtx_unlock(&nd6_mtx);	/* DF-5847 */
				if (regen_tmpaddr(ia6) == 0)
					regen = 1;
				mtx_lock(&nd6_mtx);
			}

			/*
			 * DF-5847: in6_purgeaddr() tears down routes and
			 * reaches nd6_rtrequest(), which now takes nd6_mtx.
			 * The in6_ifaddr chain is only mutated on netisr0
			 * (ioctls are dispatched there), so dropping nd6_mtx
			 * here does not expose the ia6 walk on this system.
			 */
			mtx_unlock(&nd6_mtx);
			in6_purgeaddr(&ia6->ia_ifa);
			mtx_lock(&nd6_mtx);
"""))

# ---- 7. timer: drain the deferred-free list at end of the walk ----
subs.append((
"""	/* expire prefix list */
	pr = nd_prefix.lh_first;
	while (pr) {""",
"""	/*
	 * DF-5847: release tombstoned llinfo entries now that the walk is
	 * over and no other nd6_mtx-holding walker can still reference them.
	 */
	while (nd6_lnfreelist != NULL) {
		struct llinfo_nd6 *fln = nd6_lnfreelist;

		nd6_lnfreelist = fln->ln_prev;
		R_Free(fln);
	}

	/* expire prefix list */
	pr = nd_prefix.lh_first;
	while (pr) {"""))

for old, new in subs:
    if src.count(old) != 1:
        print("ANCHOR FAIL (%d hits):\n%.120r" % (src.count(old), old))
        sys.exit(1)
    src = src.replace(old, new)

open(path, "w").write(src)
print("df5847 fix: patched OK (%d hunks)" % len(subs))