⬢ DragonFlyBSD Kernel Audit
DF-2462 / df_pre.c
← back to finding ↓ download raw
/*
 * DF-2462 verification harness -- PREAMBLE.
 *
 * gen.sh (run on the guest) concatenates:
 *
 *   df_pre.c
 *   + <verbatim sys/dev/disk/iscsi/initiator/isc_sm.c>          whole file
 *   + <verbatim isc_cam.c 81-95>      ic_lost_target
 *   + <verbatim isc_cam.c 165-309>    _inq, _scsi_encap, ic_action
 *   + <verbatim iscsi_subr.c 400-436> dwl
 *   + <verbatim iscsi_subr.c 441-528> scsi_encap
 *   + <verbatim isc_subr.c 153-168>   i_freeopt
 *   + df_post.c
 *
 * into one TU compiled into the kernel as
 * dev/disk/iscsi/initiator/dfiscsi_harness.c.  The X86_64_GENERIC
 * kernel does not configure "device iscsi_initiator" (conf/files marks
 * all six driver files "optional iscsi_initiator scbus"), so the
 * verbatim copies inside this TU are the only compiled instances.
 * The driver source files are byte-for-byte the audited tree
 * (sha256-gated by build.sh against the audit tree).
 *
 * What is verified (sys/dev/disk/iscsi/initiator/isc_sm.c:724-777):
 *   ism_stop() tears a session down IN PLACE:
 *     :734 stop receiver, :739-744 wait xmitter (tsleep loop, up to
 *     hz per iteration), :748 iscsi_cleanup, :750 i_pdu_flush,
 *     :752 ic_lost_target (frees the cam path; the xpt_freeze_devq
 *     drain in ic_freeze is #if 0'd at isc_cam.c:57-63), :762
 *     destroy_dev, :765-769 mtx_uninit x5, :771 i_freeopt (frees the
 *     option strings), :772 sessions[sid]=NULL, :774
 *     sysctl_ctx_free, :777 kfree(sp).
 *   Concurrent readers that hold sp with no refcount and no lock:
 *     - ic_action() (isc_cam.c:202-309): sp = isp->sessions[target_id]
 *       at :211 with NO lock, dereferenced at :215 (ccb
 *       spriv_ptr0=sp write), :254 (sp->opt.maxluns read), and via
 *       _scsi_encap -> scsi_encap (iscsi_subr.c:441+) -> dwl +
 *       isc_qout (isc_sm.c:303) -> i_nqueue_csnd / i_prepPDU which
 *       WRITE into sp.
 *     - per-session sysctl handlers isc_dump_stats (isc_sm.c:630),
 *       isc_sysctl_targetName/Address (isc_sm.c:656/667) hold raw
 *       sp / &sp->opt.targetName as arg1 with no lifetime guarantee;
 *       the OIDs stay registered until :774.
 *
 * Interposition model (all #define renames of FUNCTIONS; the verbatim
 * text is untouched; iscsivar.h's debug/sdebug MACROS cannot be
 * renamed because the header defines them inside the verbatim text,
 * so their kprintf() calls are intercepted instead):
 *
 *   STALL-2 (teardown parked inside the window, just after the CAM
 *   path teardown starts): ic_lost_target calls
 *   xpt_async(AC_LOST_DEVICE, ...) at isc_cam.c:90 -- the shim parks
 *   there (the real lockmgr(&isp->cam_lock) at :89 is already held;
 *   nothing in this rig or in the unprivileged sysctl read path takes
 *   cam_lock, so parking is safe).
 *
 *   VICTIM STALL (CAM, between lookup and use): ic_action's
 *   debug(4, "func_code=...") at isc_cam.c:217 compiles (via
 *   iscsivar.h:40-41) to a kprintf() call -- iscsi_debug is set to 4
 *   so it executes, and kprintf is renamed to dfh_kprintf, which
 *   parks on the "func_code" line.  That call sits right after
 *     sp = isp->sessions[target_id]  (:211)
 *     ccb_h->spriv_ptr0 = sp         (:215)
 *   and right before the sp dereferences (:254 PATH_INQ read /
 *   _scsi_encap for SCSI_IO).  Parking there deterministically models
 *   a thread preempted between lookup and use.
 *
 *   VICTIM STALL (sysctl, entered but not yet touching sp):
 *   isc_dump_stats' first ksnprintf (isc_sm.c:642) is renamed to
 *   dfh_ksnprintf which parks on the first call; the sp->stats.*
 *   reads happen while evaluating that call's arguments after the
 *   resume.
 *
 *   STALL-0: the isc_stop_receiver shim can park the teardown at
 *   isc_sm.c:734 (top of ism_stop).
 *
 *   kfree(addr, type) -> dfh_kfree: real _kfree, plus, when armed for
 *   the session chunk, an immediate reclaim of the freed chunk (same
 *   size, same malloc type -- the zone's LIFO behaviour returns the
 *   same address) filled as an attacker-controlled object:
 *     MODE_FIXUP  = attacker fully controls the former session
 *                   (valid mutexes/queues, chosen field values):
 *                   silent UAF read/write proof;
 *     MODE_FAULT  = valid mutexes, garbage queue heads: the victim's
 *                   TAILQ_INSERT_TAIL writes through 0xBEBEBEBE... ->
 *                   kernel fault (the realistic "chunk reused by an
 *                   unrelated allocation" case);
 *     MODE_STALE  = no reclaim (chunk keeps stale bytes).
 *   (dfh_kfree's body lives in df_post.c -- it needs the complete
 *   isc_session_t definition.)
 *
 *   xpt_async, xpt_free_path -> no-op shims (ic_lost_target itself is
 *     compiled VERBATIM; cam_path is a fabricated non-NULL sentinel)
 *   xpt_done -> records the CCB status (a fabricated CCB must not
 *     enter the live CAM transport)
 *   isc_stop_receiver, iscsi_cleanup, i_pdu_flush -> no-op shims
 *   release_dev, destroy_dev -> no-op shims (cdev lifecycle is not
 *     part of this finding; the cdev is fabricated for si_name only)
 *   ism_proc externals (isc_sendPDU, iscsi_* PDU workers, ...) ->
 *     link-only shims; ism_proc never runs here (the fabricated
 *     session has ISC_SM_RUN|ISC_SM_RUNNING clear so ism_stop's wait
 *     loop at :740-744 is skipped deterministically).
 *
 * Everything else is REAL: the whole ism_stop body, lockmgr on
 * sc->lock, TAILQ_REMOVE, mtx_init/mtx_uninit (INVARIANTS-checked),
 * i_freeopt's kfree of the option strings, sysctl_ctx_free (really
 * deregisters the OIDs), kmalloc/kfree(M_ISCSI), pdu_alloc from the
 * real freepdu list, isc_qout/i_prepPDU/dwl/i_nqueue_* writes into
 * sp, the verbatim xdebug() failure prints (ism_stop:774-775), and
 * the real sysctl(3) userland path for the per-session OIDs.
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/conf.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/errno.h>
#include <sys/sysctl.h>
#include <sys/queue.h>
#include <sys/kthread.h>
#include <sys/proc.h>
#include <sys/thread2.h>
#include <sys/spinlock2.h>
#include <sys/lock.h>
#include <sys/time.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <bus/cam/cam_sim.h>
#include <bus/cam/cam_xpt_sim.h>
#include <bus/cam/cam_periph.h>
#include <bus/cam/scsi/scsi_message.h>

/* forward decls for the driver types (defined by the verbatim text) */
struct isc_session;
struct pduq;

/*
 * iscsivar.h (included by the verbatim text below) DECLARES
 * iscsi_debug, iscsi_dbg_lock and M_ISCSI; they are DEFINED in
 * df_post.c (after the verbatim includes, so -Wredundant-decls is
 * happy).  iscsi_debug = 4 makes the verbatim debug(4)/sdebug(<=4)
 * prints execute -- including the ic_action "func_code" kprintf the
 * victim stall intercepts, and a natural execution trace in dmesg.
 */

/* ------------------------------------------------------------------ */
/* ---- harness state ------------------------------------------------ */
/* ------------------------------------------------------------------ */
static struct isc_session *dfh_sp;	/* the fabricated session      */
static struct cam_sim *dfh_sim;		/* fabricated sim (ic_action's) */
static struct cdev *dfh_fake_dev;	/* fabricated cdev (si_name)   */

/* stall / gate state */
static volatile int dfh_stall0_armed;	/* park in isc_stop_receiver shim */
static volatile int dfh_stall2_armed;	/* park in xpt_async shim	 */
static volatile int dfh_camstall_armed;	/* park victim in ic_action :217 */
static volatile int dfh_statsstall_armed;/* park victim in isc_dump_stats */
static volatile int dfh_stop_parked;	/* teardown thread parked	 */
static volatile int dfh_victim_parked;	/* victim thread parked	 */
static volatile int dfh_release_stop;	/* release the parked teardown  */
static volatile int dfh_release_victim;	/* release the parked victim	 */
static volatile int dfh_stop_done;	/* ism_stop returned		 */
static volatile int dfh_victim_done;	/* victim returned		 */

/* poison / reclaim state */
enum { DFH_MODE_STALE = 0, DFH_MODE_FIXUP = 1, DFH_MODE_FAULT = 2 };
static int dfh_poison_mode = DFH_MODE_STALE;
static int dfh_plant_garbage_isc;	/* FIXUP: isc = 0xAE.. (fault on
					   the stats handler's
					   sc->npdu_alloc deref --
					   natural-race panic model) */
static int dfh_poison_hits;		/* session kfrees intercepted    */
static volatile int dfh_free_seen;	/* session chunk passed kfree    */
static void *dfh_reclaim;		/* reclaim chunk (== old sp addr) */
static void *dfh_freepdu_store;		/* backing store for freepdu     */

/* results */
#define DFH_OUT_MAX 8192
static char dfh_out[DFH_OUT_MAX];
static size_t dfh_out_len;
static struct lock dfh_out_lock;

static char dfh_req_buf[2048];
static size_t dfh_req_len;

static u_int32_t dfh_last_status;	/* xpt_done shim record	 */

static void
dfh_outp(const char *fmt, ...)
{
	__va_list ap;

	lockmgr(&dfh_out_lock, LK_EXCLUSIVE);
	if (dfh_out_len < DFH_OUT_MAX - 256) {
		__va_start(ap, fmt);
		dfh_out_len += kvsnprintf(dfh_out + dfh_out_len,
					  DFH_OUT_MAX - dfh_out_len - 1,
					  fmt, ap);
		__va_end(ap);
	}
	lockmgr(&dfh_out_lock, LK_RELEASE);
}

/*
 * Gate helpers: each flag is its own sleep channel, and setters wake
 * the channel explicitly.  (An earlier version slept every waiter on
 * one channel and prodded it with wakeup() from the wait loop -- with
 * two waiters parked concurrently the wakeups ping-ponged between
 * them and burned the iteration budget in milliseconds, so a 30s park
 * expired instantly.  Per-flag channels fix that.)
 */
static void
dfh_gate_wait(volatile int *var, int val, const char *wmesg)
{
	int n;

	for (n = 0; n < 480 && *var != val; n++)
		tsleep((void *)(uintptr_t)var, 0, wmesg, hz / 4);
	kprintf("DF2462DBG gate %s: val=%d want=%d iters=%d\n",
		wmesg, *var, val, n);
}

static void
dfh_gate_set(volatile int *var, int val)
{
	*var = val;
	wakeup((void *)(uintptr_t)var);
}

/* ------------------------------------------------------------------ */
/* ---- shim bodies -------------------------------------------------- */
/* ------------------------------------------------------------------ */
/*
 * NOTE: shims whose prototypes iscsivar.h also declares
 * (isc_stop_receiver, isc_start_receiver, isc_sendPDU, i_pdu_flush,
 * iscsi_*, scsi_decap, ic_fullfeature) are neither declared nor
 * defined here -- iscsivar.h's prototypes, renamed by the #defines
 * at the bottom of this file, become their declarations, and the
 * definitions live in df_post.c (any extra declaration here would
 * trip -Wredundant-decls).  Everything defined below is
 * self-contained.
 */

static void dfh_release_dev(cdev_t d) { }
static void dfh_destroy_dev(cdev_t d) { }

/*
 * STALL-2: ic_lost_target's xpt_async(AC_LOST_DEVICE, ...) at
 * isc_cam.c:90.  Parking here puts the tearing-down thread inside
 * ic_lost_target -- after :89's real lockmgr(&isp->cam_lock) -- and
 * before xpt_free_path / return, i.e. the teardown is at
 * isc_sm.c:752-in-progress, everything after it (TAILQ_REMOVE,
 * destroy_dev, mtx_uninit, i_freeopt, sessions[sid]=NULL,
 * sysctl_ctx_free, kfree) still pending: squarely inside the
 * published window with sessions[sid] still != NULL.
 */
static void
dfh_xpt_async(int code, struct cam_path *path, void *arg)
{
	if (dfh_stall2_armed) {
		dfh_stall2_armed = 0;
		dfh_gate_set(&dfh_stop_parked, 1);
		dfh_gate_wait(&dfh_release_stop, 1, "dfhs2");
		dfh_release_stop = 0;
	}
}

static void dfh_xpt_free_path(struct cam_path *path) { }

static void
dfh_xpt_done(union ccb *ccb)
{
	/* record, do not hand a fabricated CCB to the live transport */
	dfh_last_status = ccb->ccb_h.status;
}

/* declaration only: the body (reclaim/poison) is in df_post.c */
static void	 dfh_kfree(void *addr, struct malloc_type *type);

/*
 * VICTIM STALL (sysctl): isc_dump_stats' first ksnprintf
 * (isc_sm.c:642) has not yet read sp->stats.* -- the reads happen
 * while evaluating THIS call's arguments after the resume.
 */
static int
dfh_ksnprintf(char *str, size_t size, const char *fmt, ...)
{
	__va_list ap;
	int r;

	if (dfh_statsstall_armed) {
		dfh_statsstall_armed = 0;
		dfh_gate_set(&dfh_victim_parked, 1);
		dfh_gate_wait(&dfh_release_victim, 1, "dfhsv");
		dfh_release_victim = 0;
	}
	__va_start(ap, fmt);
	r = kvsnprintf(str, size, fmt, ap);
	__va_end(ap);
	return r;
}

/*
 * VICTIM STALL (CAM) + passthrough: ic_action's debug(4,
 * "func_code=...") at isc_cam.c:217 becomes kprintf("%s: func_code=...")
 * through iscsivar.h:40-41 (iscsi_debug >= 4).  Parking on that
 * exact line models a thread preempted between the sessions[] lookup
 * (:211/:215) and the sp dereferences (:254 / _scsi_encap).
 * Everything else prints normally (the verbatim execution trace).
 */
static void
dfh_kprintf(const char *fmt, ...)
{
	__va_list ap;

	if (dfh_camstall_armed && strstr(fmt, "func_code") != NULL) {
		dfh_camstall_armed = 0;
		dfh_gate_set(&dfh_victim_parked, 1);
		dfh_gate_wait(&dfh_release_victim, 1, "dfhcv");
		dfh_release_victim = 0;
	}
	__va_start(ap, fmt);
	kvprintf(fmt, ap);
	__va_end(ap);
}

/* ------------------------------------------------------------------ */
/* ---- the interposition renames ------------------------------------ */
/* ------------------------------------------------------------------ */
#undef kfree
#undef kprintf

#define kfree(addr, type)	dfh_kfree((addr), (type))
#define kprintf(fmt, args...)	dfh_kprintf((fmt), ## args)
#define ksnprintf(s, z, f, a...)	dfh_ksnprintf((s), (z), (f), ## a)

#define isc_stop_receiver	dfh_isc_stop_receiver
#define iscsi_cleanup		dfh_iscsi_cleanup
#define i_pdu_flush		dfh_i_pdu_flush
#define release_dev		dfh_release_dev
#define destroy_dev		dfh_destroy_dev
#define isc_sendPDU		dfh_isc_sendPDU
#define iscsi_requeue		dfh_iscsi_requeue
#define iscsi_async		dfh_iscsi_async
#define iscsi_reject		dfh_iscsi_reject
#define iscsi_r2t		dfh_iscsi_r2t
#define iscsi_done		dfh_iscsi_done
#define scsi_decap		dfh_scsi_decap
#define ic_fullfeature		dfh_ic_fullfeature
#define isc_start_receiver	dfh_isc_start_receiver
#define xpt_async		dfh_xpt_async
#define xpt_free_path		dfh_xpt_free_path
#define xpt_done		dfh_xpt_done

/*
 * The verbatim driver text follows in gen.sh's concatenation.
 */