DragonFlyBSD Kernel Audit
DF-0676 / ng_o2m_df.c
← back to finding ↓ download raw
/*
 * DF-0676 harness: deterministically prove the double-free in
 * ng_one2many_rcvdata()'s XMIT_ALL error path
 * (sys/netgraph7/one2many/ng_one2many.c:464-468).
 *
 * Bug mechanism (traced against netgraph7/netgraph.h):
 *   m = NGI_M(item);                      // PEEK: mbuf still owned by item
 *   m2 = m_dup(m, M_NOWAIT);
 *   if (m2 == NULL) {                     // OOM
 *       NG_FREE_ITEM(item);  // -> el_flags |= NGQF_FREE; does NOT clear _NGI_M(item)
 *       NG_FREE_M(m);        // -> m_freem(m); local m=NULL; _NGI_M(item) STILL = freed mbuf
 *       return (ENOBUFS);
 *   }
 *   // back in ng_apply_item() -> ng_unref_item(item, ENOBUFS) -> ng_free_item(item):
 *   //   case NGQF_DATA: NG_FREE_M(_NGI_M(item));  <-- m_freem() on already-freed mbuf
 *   //                                                 => DOUBLE FREE
 *
 * Live triggering needs m_dup() to fail (mbuf OOM), which is not reliable to force
 * without destabilising the guest. This harness instead replays the EXACT macro
 * sequence from netgraph7/netgraph.h on a real mbuf + a real struct ng_item, so the
 * double-free is reproduced deterministically. On GENERIC (INVARIANTS on) the second
 * m_freem() trips the slab/mbuf double-free check and panics.
 *
 * Reachability: building the one2many topology requires the NG_CONTROL socket, whose
 * ngc_attach() does caps_priv_check(SYSCAP_RESTRICTEDROOT) -> ROOT-ONLY.
 *
 * Run: kldload ./ng_o2m_df.ko   -> double-free panic.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>

#include <netgraph7/netgraph.h>

MALLOC_DEFINE(M_DF0676, "df0676", "DF-0676 ng double-free harness");

static int apply_fix = 0;	/* =1 apply the fix (NGI_GET_M detach); =0 baseline (buggy) */
TUNABLE_INT("hw.df0676.apply_fix", &apply_fix);

static int
ng_o2m_df_load(module_t mod, int what, void *arg)
{
	struct mbuf *m;
	struct ng_item *item;
	void *freed_mbuf;

	if (what != MOD_LOAD)
		return (0);

	/* Build a real mbuf and a real NGQF_DATA item wrapping it (m = NGI_M(item)). */
	m = m_gethdr(M_WAITOK, MT_DATA);
	if (m == NULL) { kprintf("DF0676: m_gethdr failed\n"); return 0; }
	mtod(m, char *)[0] = 'X';

	item = kmalloc(sizeof(*item), M_DF0676, M_WAITOK | M_ZERO);
	item->el_flags = NGQF_DATA;	/* type = data */
	item->refs = 1;
	_NGI_M(item) = m;		/* peek: mbuf owned by item (mirrors line 425) */

	kprintf("DF0676: m=%p item=%p  _NGI_M(item)=%p  el_flags=0x%lx\n",
	    m, item, _NGI_M(item), item->el_flags);

	/* --- ng_one2many_rcvdata XMIT_ALL error path (m_dup == NULL) --- */
	kprintf("DF0676: replaying XMIT_ALL m_dup-NULL error path (apply_fix=%d):\n",
	    apply_fix);
	if (apply_fix) {
		/* THE FIX: detach the mbuf from the item before freeing it. */
		NGI_GET_M(item, m);	/* m = _NGI_M(item); _NGI_M(item) = NULL */
		kprintf("DF0676:   [FIX] NGI_GET_M(item,m) detached mbuf (_NGI_M=NULL)\n");
	}
	kprintf("DF0676:   NG_FREE_ITEM(item)  [marks NGQF_FREE%s]\n",
	    apply_fix ? "; _NGI_M already NULL" : ", does NOT clear _NGI_M");
	NG_FREE_ITEM(item);		/* el_flags |= NGQF_FREE */
	kprintf("DF0676:   NG_FREE_M(m)        [m_freem(m)]\n");
	NG_FREE_M(m);			/* frees the mbuf */
	kprintf("DF0676: after error path: local m=%p  _NGI_M(item)=%p (DANGLING -> freed mbuf)\n",
	    m, _NGI_M(item));

	/* --- ng_apply_item -> ng_unref_item -> ng_free_item (NGQF_DATA case) --- */
	kprintf("DF0676: replaying ng_apply_item -> ng_unref_item (refs->0) -> ng_free_item:\n");
	kprintf("DF0676:   NGQF_DATA case: NG_FREE_M(_NGI_M(item))  <-- DOUBLE FREE\n");
	item->refs = 0;			/* emulate last reference release */
	/* This is the exact NGQF_DATA line from ng_free_item(): frees _NGI_M again. */
	freed_mbuf = _NGI_M(item);	/* the about-to-be-double-freed address */
	NG_FREE_M(_NGI_M(item));

	kprintf("DF0676: ng_free_item freed mbuf %p a 2nd time (double-free executed)\n",
	    freed_mbuf);
	kprintf("DF0676: mbuf objcache did NOT trap the double-free (silent corruption)\n");

	/* Prove the double-free corrupted the free list: the address handed back twice
	 * should now be allocatable as two DISTINCT mbufs (aliasing). Pin to one CPU. */
	crit_enter();
	{
		struct mbuf *q[16];
		int i, hits = 0;
		for (i = 0; i < 16; i++) {
			q[i] = m_gethdr(M_WAITOK, MT_DATA);
			if ((void *)q[i] == (void *)freed_mbuf) hits++;
		}
		kprintf("DF0676: 16 fresh m_gethdr: address %p returned %d time(s); "
		    ">=2 => double-free aliasing (use-after-free primitive)\n",
		    freed_mbuf, hits);
		for (i = 0; i < 16; i++)
			m_freem(q[i]);
	}
	crit_exit();

	kfree(item, M_DF0676);
	return (0);
}

static moduledata_t ng_o2m_df_mod = { "ng_o2m_df", ng_o2m_df_load, NULL };
DECLARE_MODULE(ng_o2m_df, ng_o2m_df_mod, SI_SUB_PSEUDO, SI_ORDER_ANY);
MODULE_VERSION(ng_o2m_df, 1);