DragonFlyBSD Kernel Audit
DF-0796 / df0796_harness.c
← back to finding ↓ download raw
/*
 * 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). */