โฌข DragonFlyBSD Kernel Audit
DF-3460 / df3460_harness.c
โ† back to finding โ†“ download raw
/*
 * DF-3460 โ€” rssadapt_sysctl_interval() UAF across vap teardown
 * =============================================================
 *
 * Target: sys/netproto/802_11/wlan/ieee80211_rssadapt.c:340-353 (handler)
 *         sys/netproto/802_11/wlan/ieee80211_rssadapt.c:151-155 (rssadapt_deinit)
 *
 * Lifetime chain (all on this DragonFly tree):
 *
 *   attach : iwm_llat... vap create -> ieee80211_ratectl_init()
 *            (ieee80211_ratectl.c:119-135) -> rssadapt_init()
 *            (ieee80211_rssadapt.c:128-149) kmalloc's vap->iv_rs AND
 *            registers the world-readable sysctl oid
 *            net.wlan.N.rssadapt_rate_interval  (rssadapt_sysctlattach :356-362)
 *
 *   teardown: iwm_vap_delete() (if_iwm.c:6486-6493) calls
 *            ieee80211_ratectl_deinit(vap)  ->  rssadapt_deinit()  ->
 *            IEEE80211_FREE(vap->iv_rs, M_80211_RATECTL)   [iv_rs NOT nulled]
 *            ... and only LATER ieee80211_vap_detach() -> ... ->
 *            ieee80211_sysctl_vdetach(vap)   (ieee80211.c:827, the LAST detach)
 *            removes the oid.
 *
 *   During the whole window between the kfree and the sysctl-ctx teardown
 *   (which spans ether_ifdetach, ieee80211_stop, task drains, node detach,
 *   ...), the oid stays registered and world-readable, and its handler does
 *
 *       struct ieee80211_rssadapt *rs = vap->iv_rs;   // dangling
 *       int msecs = ticks_to_msecs(rs->interval);     // UAF READ
 *       ... sysctl_handle_int(...)                    // 4 bytes -> user
 *       rssadapt_setinterval(vap, msecs);             // UAF WRITE (root)
 *
 *   An unprivileged user polling `sysctl net.wlan.0.rssadapt_rate_interval`
 *   while the interface is being destroyed reads 4 bytes out of a FREED
 *   (and possibly reused) M_80211_RATECTL chunk; the write path (root) writes
 *   4 controlled bytes into the freed chunk.
 *
 *   LATENT, same as DF-0730/DF-0731: no in-tree driver selects RSSADAPT
 *   (default is AMRR) and the audit guest has no WiFi radio โ€” but the amrr
 *   twin (ieee80211_amrr.c:390-403 amrr_sysctl_interval + SYSCTL_ADD_UINT
 *   pointing directly into the freed chunk) is the DEFAULT algorithm and has
 *   the identical deinit-order hole on any real WiFi system.
 *
 * ------------------------------------------------------------------------
 * Harness: replicates the exact lifetime.  malloc'd iv_rs, "kernel" free at
 * deinit (NOT nulled โ€” as master), immediate same-size reallocation with a
 * poison object (kmalloc reuses freed chunks), then the sysctl read handler
 * โ€” the value handed to the unprivileged reader is the POISON, i.e. bytes
 * from a different live object that reused the chunk.  -DFIXED applies this
 * finding's fix.diff (deinit NULLs iv_rs, handler returns ENXIO).
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define ENXIO_ 6

struct ieee80211_rssadapt {
	const void	*vap;
	int		interval;	/* in ticks */
};

struct fake_vap {
	const char		*name;
	struct ieee80211_rssadapt *iv_rs;
};

/* poison object that reuses the freed chunk (any 16-byte kmalloc) */
struct poison {
	uint32_t	marker0;
	uint32_t	marker1;
	uint32_t	marker2;
	uint32_t	marker3;
};

static int sysctl_out_val;		/* what the unprivileged reader gets */

/* replica of rssadapt_sysctl_interval (read path), :341-353 */
static int
rssadapt_sysctl_interval_read(struct fake_vap *vap)
{
	struct ieee80211_rssadapt *rs = vap->iv_rs;
	int msecs, error;

#ifdef FIXED
	if (rs == NULL)
		return (ENXIO_);	/* fix: refuse before deref */
#endif
	msecs = rs->interval;		/* ticks_to_msecs() in the kernel */
	sysctl_out_val = msecs;
	error = 0;
	return error;
}

/* NOTE: built -O0 (see build.sh).  At -O2 gcc treats the post-free
 * dereference as unreachable UB and CSEs the second load of rs->interval
 * with the pre-free one, hiding the chunk reuse.  The kernel does no
 * such elision across the real sysctl dispatch. */

/* replica of rssadapt_setinterval write path (root-only) */
static int
rssadapt_sysctl_interval_write(struct fake_vap *vap, int msecs)
{
	struct ieee80211_rssadapt *rs = vap->iv_rs;

#ifdef FIXED
	if (rs == NULL)
		return (ENXIO_);
#endif
	/* rssadapt_setinterval: clamp + store */
	if (msecs < 100)
		msecs = 100;
	rs->interval = msecs;		/* UAF WRITE into freed chunk */
	return 0;
}

int
main(void)
{
	struct fake_vap vap;
	struct poison *p;

	vap.name = "wlan0";

	/* ---- vap attach: rssadapt_init ---- */
	vap.iv_rs = malloc(sizeof(*vap.iv_rs));
	vap.iv_rs->interval = 500;	/* 500ms default */
	printf("attach          : iv_rs=%p interval=%d\n",
	       (void *)vap.iv_rs, vap.iv_rs->interval);

	/* normal unprivileged read while alive */
	rssadapt_sysctl_interval_read(&vap);
	printf("read (alive)    : reader gets %d  (ok)\n", sysctl_out_val);

	/* ---- vap destroy, step 1: iwm_vap_delete -> rssadapt_deinit ---- */
	free(vap.iv_rs);		/* NOTE: iv_rs NOT cleared (master) */
#ifdef FIXED
	vap.iv_rs = NULL;		/* fix.diff: rssadapt_deinit NULLs it */
#endif
	printf("deinit          : iv_rs freed, pointer now %p (%s)\n",
	       (void *)vap.iv_rs, vap.iv_rs ? "dangling" : "cleared");

	/* ---- the window: chunk gets reused by unrelated 16-byte alloc ---- */
	p = malloc(sizeof(struct poison));
	printf("window          : chunk reused by poison obj at %p\n",
	       (void *)p);
	if (p == NULL) {
		fprintf(stderr, "alloc failed\n");
		return 2;
	}
	/* poison the WHOLE replacement object; struct ieee80211_rssadapt's
	 * .interval lives at offset 8 (after the vap pointer) == marker2 */
	memset(p, 0xBD, sizeof(*p));
	p->marker2 = 0xDEADBEEF;

	/* ---- unprivileged sysctl read lands INSIDE the teardown window ---- */
	if (rssadapt_sysctl_interval_read(&vap) == ENXIO_) {
		printf("read (window)   : handler refused (fix active)\n");
		printf("RESULT: FIXED โ€” no UAF read/write\n");
		return 0;
	}
	printf("read (window)   : unprivileged reader gets 0x%x from FREED "
	       "chunk\n", sysctl_out_val);

	if (sysctl_out_val == 0xDEADBEEF)
		printf("  -> UAF READ confirmed: reader saw the replacement "
		       "object's bytes (4-byte kernel-heap info leak)\n");

	/* ---- root write path in the same window ---- */
	rssadapt_sysctl_interval_write(&vap, 300);
	printf("write (window)  : poison.marker2 (offset 8, = interval) "
	       "now 0x%08x (was 0xDEADBEEF)\n", p->marker2);
	if (p->marker2 == 300)
		printf("  -> UAF WRITE confirmed: root sysctl write landed in "
		       "the replacement object\n");

	printf("RESULT: BUG REPRODUCED โ€” sysctl handler dereferences freed "
	       "iv_rs across vap teardown\n");
	return 0;
}