⬢ DragonFlyBSD Kernel Audit
DF-3806 / msk_callout_replica.c
← back to finding ↓ download raw
/*
 * msk_callout_replica.c - deterministic replica of if_msk.c's tick protocol.
 *
 * if_msk.c arms ONE NON-MPSAFE callout (callout_init, if_msk.c:1375):
 *
 *   msk_tick()  if_msk.c:2930-2946
 *       lwkt_serialize_enter(ifp->if_serializer)   BLOCKING   (:2936)
 *       mii_tick(mii) (:2940) / msk_miibus_statchg (:2941-2942)
 *       callout_reset(&sc_if->msk_tick_ch, hz, msk_tick, sc_if)  (:2943)
 *       lwkt_serialize_exit                                  (:2945)
 *
 *   msk_stop()  if_msk.c:3675-3823  (ASSERT_SERIALIZED :3687)
 *       callout_stop(&sc_if->msk_tick_ch)                     (:3689)
 *
 * msk_stop() is called with the serializer held from:
 *       msk_init (:3352; reached from msk_ioctl SIOCSIFFLAGS up/down
 *       :880-881/:895, watchdog :2663/:2686), msk_detach (:1762-1764),
 *       mskc_shutdown (:2699-2701), mskc_suspend (:2719-2722),
 *       mskc_resume -> msk_init (:2756).
 *
 * DragonFly callout_stop() is the SYNCHRONOUS variant
 * (sys/kern/kern_timeout.c:1091 -> _callout_cancel_or_stop(..., sync=1));
 * when the callback is INPROG it ssleep()s on "costp" (kern_timeout.c:923)
 * until the callback completes.  If msk_tick is INPROG and parked inside
 * lwkt_serialize_enter on the serializer held by the msk_stop() caller,
 * neither thread can make progress: PERMANENT AB-BA DEADLOCK.
 * (msk's callout is NON-MPSAFE, so the parked softclock callback also
 * holds the MP lock while it waits - extra system-wide degradation.)
 *
 * This module reproduces exactly that protocol with two callouts:
 *   "msk tick (as shipped)"     - blocking acquire + self re-arm at hz
 *   "msk tick (fix.diff shape)" - lwkt_serialize_try + 1-tick retry
 * and exposes sysctls to run the interleavings:
 *
 *   kern.mskrep.arm      =1  arm both replicas
 *   kern.mskrep.stop_try =1  hold serializer, callout_stop() the
 *                            try-based (fixed) shape -> must return,
 *                            pending=0 (validates the fix mechanism)
 *   kern.mskrep.stop_blk =1  hold serializer, wait for the shipped-shape
 *                            handler to be BLOCKED on it, callout_stop()
 *                            -> if the analysis is right this NEVER
 *                            returns (sysctl thread D-state wchan "costp").
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/serialize.h>
#include <sys/callout.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <sys/thread.h>

static struct lwkt_serialize ser;	/* == ifp->if_serializer */
static struct callout tick_blk_c;	/* msk_tick as shipped */
static struct callout tick_try_c;	/* msk_tick with fix.diff shape */

static volatile int blk_fired, blk_ran;
static volatile int try_fired, try_ran;

/* if_msk.c:2930-2946 - msk_tick() EXACT protocol (blocking) */
static void
msk_tick_blk(void *arg)
{
	blk_fired++;
	/* if_msk.c:2936 - BLOCKING acquire */
	lwkt_serialize_enter(&ser);
	blk_ran++;		/* mii_tick/statchg live here (:2940-2942) */
	/* if_msk.c:2943 - self re-arm at hz */
	callout_reset(&tick_blk_c, hz, msk_tick_blk, NULL);
	lwkt_serialize_exit(&ser);
}

/* msk_tick() with the proposed fix: try-enter, 1-tick retry */
static void
msk_tick_try(void *arg)
{
	try_fired++;
	if (lwkt_serialize_try(&ser)) {
		try_ran++;
		callout_reset(&tick_try_c, hz, msk_tick_try, NULL);
		lwkt_serialize_exit(&ser);
	} else {
		/* serializer held (e.g. by msk_stop) - retry next tick */
		callout_reset(&tick_try_c, 1, msk_tick_try, NULL);
	}
}

static int
arm_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) {
		blk_fired = blk_ran = try_fired = try_ran = 0;
		callout_reset(&tick_try_c, 1, msk_tick_try, NULL);
		callout_reset(&tick_blk_c, 1, msk_tick_blk, NULL);
		kprintf("mskrep: armed (hz=%d)\n", hz);
	}
	return 0;
}

/*
 * Fix-shape test: msk_stop() vs the try-based msk_tick.
 * callout_stop() must return (handler never blocks) and unload the
 * racing re-arm.  Validates the DF-3806 fix.diff mechanism.
 */
static int
stop_try_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0, tries;

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

	for (tries = 0; tries < 100; tries++) {
		lwkt_serialize_enter(&ser);		/* as msk_stop :3687 */
		if (try_fired == try_ran &&
		    callout_pending(&tick_try_c) == 0)
			tsleep(&try_fired, 0, "tryw", 1);
		kprintf("mskrep[try]: stop (fired=%d ran=%d)\n",
		    try_fired, try_ran);
		callout_stop(&tick_try_c);		/* :3689 */
		kprintf("mskrep[try]: callout_stop returned, "
		    "pending=%d\n", callout_pending(&tick_try_c));
		lwkt_serialize_exit(&ser);
		if (callout_pending(&tick_try_c) == 0)
			break;
		tsleep(&try_ran, 0, "tryr", hz / 10);
	}
	kprintf("mskrep[try]: RESULT pending=%d "
	    "(0 == try-shape fully stopped, no deadlock)\n",
	    callout_pending(&tick_try_c));
	return 0;
}

/*
 * Shipped-shape test: msk_stop() vs the blocking msk_tick.
 * Take the serializer (as msk_stop does), wait until the handler is
 * parked inside lwkt_serialize_enter (fired but not ran), then
 * callout_stop().  If the AB-BA analysis is right this never prints
 * the next line - the sysctl thread AND a softclock thread are wedged
 * permanently (D-state, wchan "costp" == kern_timeout.c:923), and the
 * parked non-MPSAFE callback additionally holds the MP lock.
 */
static int
stop_blk_sysctl(SYSCTL_HANDLER_ARGS)
{
	int error, v = 0, tries;

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

	lwkt_serialize_enter(&ser);			/* as msk_stop :3687 */
	/* Wait until the handler is parked inside lwkt_serialize_enter
	 * (fired but not ran).  Spin+yield against 'ticks' (advanced by
	 * hardclock, NOT softclock), NOT tsleep(): a sleeping timeout
	 * would itself be delivered by a softclock thread, and the
	 * parked NON-MPSAFE callback takes out both the softclock
	 * thread and the MP lock of the CPU it parks on (observed on
	 * the first run: sysctl stuck D1 'blkw', softclock B1 'slize').
	 * Wait up to 10 s - the tick self re-arms at hz (1 s period). */
	{
		int start = ticks;
		long spin = 0;

		while (blk_ran == blk_fired && ticks - start < 10 * hz) {
			if ((++spin & 0xffff) == 0)
				lwkt_user_yield();
		}
		tries = ticks - start;
	}
	kprintf("mskrep[blk]: msk_tick handler BLOCKED on our "
	    "serializer (fired=%d ran=%d waited=%d) - entering "
	    "callout_stop()\n", blk_fired, blk_ran, tries);
	kprintf("mskrep[blk]: IF THIS IS THE LAST mskrep LINE, "
	    "THE if_msk.c AB-BA DEADLOCK IS PROVEN\n");
	callout_stop(&tick_blk_c);			/* :3689 */
	kprintf("mskrep[blk]: callout_stop returned (NO deadlock)\n");
	lwkt_serialize_exit(&ser);
	return 0;
}

static SYSCTL_NODE(_kern, OID_AUTO, mskrep, CTLFLAG_RW, 0,
    "msk callout replica");
SYSCTL_PROC(_kern_mskrep, OID_AUTO, arm, CTLTYPE_INT | CTLFLAG_RW, 0, 0,
    arm_sysctl, "I", "arm replica callouts");
SYSCTL_PROC(_kern_mskrep, OID_AUTO, stop_try, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, stop_try_sysctl, "I", "msk_stop vs try-based (fixed) msk_tick");
SYSCTL_PROC(_kern_mskrep, OID_AUTO, stop_blk, CTLTYPE_INT | CTLFLAG_RW,
    0, 0, stop_blk_sysctl, "I", "msk_stop vs shipped (blocking) msk_tick");

static int
msk_replica_load(struct module *m, int what, void *arg)
{
	switch (what) {
	case MOD_LOAD:
		lwkt_serialize_init(&ser);
		/* if_msk.c:1375 - callout_init(&sc_if->msk_tick_ch):
		 * NON-MPSAFE, exactly as the driver does it. */
		callout_init(&tick_try_c);
		callout_init(&tick_blk_c);
		kprintf("msk_callout_replica: loaded (hz=%d ncpus=%d)\n",
		    hz, ncpus);
		return 0;
	case MOD_UNLOAD:
		/* only safe if the AB-BA test was not run */
		callout_stop(&tick_try_c);
		callout_stop(&tick_blk_c);
		return 0;
	}
	return 0;
}

static moduledata_t msk_replica_mod = {
	"msk_callout_replica",
	msk_replica_load,
	NULL
};
DECLARE_MODULE(msk_callout_replica, msk_replica_mod, SI_SUB_DRIVERS,
    SI_ORDER_ANY);