⬢ DragonFlyBSD Kernel Audit
DF-3982 / sbp_replica.c
← back to finding ↓ download raw
/*
 * sbp_replica.c - mechanism replica for the two sbp.c(4) pass-2 findings.
 *
 * MECHANISM A (DF-3981: login_callout UAF):
 *   sbp.c arms sdev->login_callout (callout_reset, sbp.c:649) and then frees
 *   sdev with kmalloc'd-container callout still armed on three paths
 *   (sbp_alloc_lun stale-LUN sbp.c:444-445, !VALID_LUN loop sbp.c:536-539,
 *   sbp_post_explore GC -> sbp_free_target:2002 -> sbp_free_sdev:1989)
 *   without callout_stop().  This replica proves the DragonFly callout
 *   subsystem fires the callback (and post-processes the external struct
 *   callout, i.e. writes into the freed slab) after the container is freed:
 *
 *     sysctl kern.sbpprep.a=1  -> kmalloc obj, arm 2*hz callout, kfree obj,
 *                                 report; callback later kprintf's from the
 *                                 freed object (arg == the freed pointer).
 *
 * MECHANISM B (DF-3982: blocking callout_stop from interrupt/crit context):
 *   sbp_recv1 (hard-interrupt context: fwohci_intr -> fwohci_intr_body ->
 *   fwohci_arcv(crit_enter, fwohci.c:2688) -> fw_rcv -> fw_xfer_done ->
 *   sbp_recv(crit_enter, sbp.c:1823)) calls callout_stop() at sbp.c:1600,
 *   2590 (via sbp_dequeue_ocb) and 2705 (via sbp_abort_ocb).  DragonFly's
 *   current callout_stop() is synchronous (kern_timeout.c:1091 -> sync=1 ->
 *   ssleep(..., "costp", ...) at kern_timeout.c:923) whenever the callback is
 *   queued or INPROG on any CPU.  This replica proves callout_stop() SLEEPS
 *   ('costp') for as long as the callback remains in progress - including
 *   when the caller holds a critical section, which is exactly the state
 *   sbp_recv1 is in:
 *
 *     sysctl kern.sbpprep.armb=1   -> arm 1-tick callout whose callback
 *                                     sleeps 5s (mimic sbp_mgm_timeout ->
 *                                     sbp_reset_start -> fw_asyreq work)
 *     sysctl kern.sbpprep.stopb=1  -> from inside crit_enter(), wait for
 *                                     callback INPROG, then callout_stop();
 *                                     kprintf timestamps around it.  This
 *                                     thread must disappear into 'costp' for
 *                                     ~5s - sleeping inside a critical
 *                                     section, which the hard-interrupt
 *                                     sbp_recv1 path may not do.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/malloc.h>
#include <sys/callout.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/thread2.h>
#include <sys/time.h>
#include <sys/globaldata.h>	/* mycpuid */

MALLOC_DEFINE(M_SBPPREP, "sbpprep", "sbp mechanism replica");

/* ---------------- Mechanism A: callout survives kfree ---------------- */

struct fake_sdev {
	char		pad[64];	/* stand-in for sbp_dev head */
	struct callout	login_callout;	/* sbp.c:183 */
	char		tail[64];
};

static struct fake_sdev *a_obj;
static volatile int a_freed, a_fired;

static void
a_login_callout(void *arg)
{
	/*
	 * Real code (sbp_login_callout, sbp.c:625-629) immediately does
	 * sbp_mgm_orb(sdev,...) on this pointer.  We only *report* here so the
	 * guest stays healthy: the point is that this fires at all after
	 * kfree().
	 */
	a_fired++;
	kprintf("sbpprep[A]: login_callout FIRED on %s object %p "
	    "(arg == freed pointer: %s)\n",
	    a_freed ? "FREED" : "live", arg, arg == (void *)a_obj ? "YES" : "?");
}

static int
a_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v == 1) {
		a_fired = a_freed = 0;
		a_obj = kmalloc(sizeof(*a_obj), M_SBPPREP, M_WAITOK | M_ZERO);
		callout_init(&a_obj->login_callout);
		callout_reset(&a_obj->login_callout, 2 * hz,
		    a_login_callout, a_obj);
		kprintf("sbpprep[A]: armed login_callout in %p, tick=%d\n",
		    a_obj, ticks);
		/*
		 * sbp_free_sdev (sbp.c:1978-1990) has no callout_stop():
		 * destroy the DMA map, free the dma, kfree(sdev) - callout
		 * left armed.
		 */
		kfree(a_obj, M_SBPPREP);
		a_freed = 1;
		kprintf("sbpprep[A]: kfree(%p) DONE - callout still armed, "
		    "no callout_stop() (sbp.c:1978 has none)\n", a_obj);
	}
	return 0;
}

/* ------------- Mechanism B: blocking callout_stop in crit ------------- */

static struct callout b_c;			/* mgm_ocb_timeout replica */
static volatile int b_started, b_done;
static int b_stop_elapsed_ms;
static volatile int b_in_stop;
static struct sysctl_ctx_list sbpprep_sysctl_ctx;

static void
b_mgm_timeout(void *arg)
{
	b_started++;
	/*
	 * sbp_mgm_timeout (sbp.c:2126-2146) itself does kprintf +
	 * sbp_reset_start -> sbp_write_cmd -> fw_asyreq, whose completion
	 * depends on bus answers (tlabel allocation, retries) - i.e. an
	 * unbounded body.  Stand in a body that completes only when the
	 * controller (kern.sbpprep_wakeb) releases it.
	 */
	kprintf("sbpprep[B]: mgm_timeout replica callback STARTED "
	    "(INPROG set, cpu=%d)\n", mycpuid);
	tsleep(&b_done, 0, "bcb", 0);	/* no timeout: woken by wakeb */
	b_done++;
	kprintf("sbpprep[B]: mgm_timeout replica callback DONE "
	    "(INPROG cleared)\n");
}

static int
armb_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v == 1) {
		b_started = b_done = 0;
		callout_reset(&b_c, 1, b_mgm_timeout, NULL);
		kprintf("sbpprep[B]: armed 1-tick mgm_ocb_timeout replica\n");
	}
	return 0;
}

static int
stopb_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;
	struct timeval t0, t1;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v != 1)
		return 0;

	/*
	 * Replicate the sbp_recv1 state: hard-interrupt path runs under
	 * crit_enter() (fwohci.c:2688 + sbp.c:1823).  The callback is
	 * already INPROG (armed ~2s ago, 6s body, and it busy-waits so it
	 * cannot finish early), then callout_stop() under the critical
	 * section - exactly sbp.c:1600.
	 */
	crit_enter();
	if (!b_started) {
		crit_exit();
		kprintf("sbpprep[B]: ERROR - callback not started yet\n");
		return 0;
	}

	b_in_stop = 1;
	microtime(&t0);
	kprintf("sbpprep[B]: callout_stop() ENTERED inside crit section "
	    "(sbp.c:1600 state) at %ld.%06ld\n", (long)t0.tv_sec, t0.tv_usec);
	callout_stop(&b_c);
	microtime(&t1);
	b_stop_elapsed_ms = (t1.tv_sec - t0.tv_sec) * 1000 +
	    (t1.tv_usec - t0.tv_usec) / 1000;
	b_in_stop = 0;
	kprintf("sbpprep[B]: callout_stop() RETURNED after %d ms at "
	    "%ld.%06ld\n", b_stop_elapsed_ms,
	    (long)t1.tv_sec, t1.tv_usec);
	crit_exit();
	kprintf("sbpprep[B]: VERDICT: callout_stop %s while INPROG - "
	    "caller slept in 'costp' inside a critical section\n",
	    b_stop_elapsed_ms >= 3000 ? "BLOCKED" : "did not block");
	return 0;
}

static int
wakeb_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0;

	error = sysctl_handle_int(oidp, &v, 0, req);
	if (error || req->newptr == NULL)
		return error;
	if (v != 1)
		return 0;
	/*
	 * Simulated bus answer releasing the mgm callback after >=3s of the
	 * caller being stuck in 'costp'.
	 */
	kprintf("sbpprep[B]: wakeb: releasing callback in 3s "
	    "(stopper has been in 'costp' all this time)\n");
	tsleep(&b_done, 0, "wkdelay", 3 * hz);
	wakeup(&b_done);
	kprintf("sbpprep[B]: wakeb: wakeup(&b_done) issued\n");
	return 0;
}

static int
sbpprep_modevent(module_t mod, int type, void *data)
{
	switch (type) {
	case MOD_LOAD:
		callout_init(&b_c);
		sysctl_ctx_init(&sbpprep_sysctl_ctx);
		SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx,
		    SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO,
		    "sbpprep_a", CTLTYPE_INT|CTLFLAG_RW, 0, 0, a_sysctl, "I",
		    "A: arm+free callout container (DF-3981 mechanism)");
		SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx,
		    SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO,
		    "sbpprep_armb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, armb_sysctl,
		    "I", "B: arm mgm-timeout replica (DF-3982 mechanism)");
		SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx,
		    SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO,
		    "sbpprep_stopb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, stopb_sysctl,
		    "I", "B: callout_stop under crit while INPROG");
		SYSCTL_ADD_PROC(&sbpprep_sysctl_ctx,
		    SYSCTL_STATIC_CHILDREN(_kern), OID_AUTO,
		    "sbpprep_wakeb", CTLTYPE_INT|CTLFLAG_RW, 0, 0, wakeb_sysctl,
		    "I", "B: release the blocked callback");
		kprintf("sbpprep: loaded\n");
		break;
	case MOD_UNLOAD:
		callout_stop(&b_c);
		sysctl_ctx_free(&sbpprep_sysctl_ctx);
		kprintf("sbpprep: unloaded\n");
		break;
	default:
		return EOPNOTSUPP;
	}
	return 0;
}

static moduledata_t sbpprep_mod = { "sbpprep", sbpprep_modevent, 0 };
DECLARE_MODULE(sbpprep, sbpprep_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);