Files
2026-07-13 13:04:25 +08:00

513 lines
16 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Kernel Address Sanitizer (KASAN) runtime for ESP-IDF.
*
* GCC -fsanitize=kernel-address instruments loads/stores with calls to
* __asan_load<N>_noabort / __asan_store<N>_noabort. These stubs check a
* shadow memory region and report a violation if poisoned memory is accessed.
*
* Shadow layout (nibble-based):
* One shadow byte covers 8 bytes of real memory via two 4-bit nibbles.
* Low nibble (bits 0-3) → real bytes 0-3; high nibble (bits 4-7) → 4-7.
* Shadow address = shadow_offset + (real_addr >> 3)
* Nibble select = (real_addr >> 2) & 1
*
* Nibble values:
* 0x0 all 4 bytes valid
* 0x1-0x3 first N bytes valid (partial granule)
* 0xC uninitialised / never allocated
* 0xD heap right redzone
* 0xE heap left redzone
* 0xF freed heap block
*
* The 4-byte granule matches TLSF's native alignment, so ROM TLSF can be used.
*
* This file MUST be compiled with -fno-sanitize=kernel-address.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdatomic.h>
#include <string.h>
#include "sdkconfig.h"
#include "esp_attr.h"
#include "esp_cpu.h"
#include "esp_rom_sys.h"
#include "esp_system.h"
#include "soc/soc.h"
#if CONFIG_COMPILER_KASAN
#define KASAN_SHADOW_MAP_BASE ((uintptr_t)SOC_DRAM_LOW)
#define KASAN_SHADOW_SIZE ((size_t)((((uintptr_t)SOC_DRAM_HIGH - (uintptr_t)SOC_DRAM_LOW) + 7U) >> 3))
/*
* Shadow array — DRAM_ATTR so loads/stores remain valid when the SPI flash
* cache is disabled (IRAM KASAN stubs still run during flash operations).
*/
DRAM_ATTR uint8_t __attribute__((aligned(4)))
kasan_shadow_mem[KASAN_SHADOW_SIZE];
/*
* Runtime shadow offset: &kasan_shadow_mem[0] - (MAP_BASE >> 3).
* DRAM_ATTR so it is accessible with cache disabled.
*/
DRAM_ATTR uintptr_t kasan_shadow_offset;
/* Nibble poison tags */
#define KASAN_NIBBLE_VALID 0x0
#define KASAN_NIBBLE_UNINIT 0xC
#define KASAN_NIBBLE_HEAP_RRZ 0xD
#define KASAN_NIBBLE_HEAP_LRZ 0xE
#define KASAN_NIBBLE_HEAP_FREE 0xF
/*
* Suppression counter for KASAN checks.
*
* Incremented (and the corresponding decrement on exit) when:
* - a report is in flight: the report path itself executes instrumented code,
* which would otherwise recurse;
* - kasan_disable_checks() is called explicitly: the panic handler disables
* checks for the remainder of crash handling, since backtrace and stack
* dumps legitimately read guard pages and poisoned redzones that would
* otherwise trigger spurious reports.
*
* Atomic so it is safe across cores and ISRs. DRAM-resident so it remains
* accessible with cache disabled.
*/
static DRAM_ATTR atomic_uint_fast32_t s_kasan_suppress_depth;
#if CONFIG_KASAN_NO_HALT
static DRAM_ATTR atomic_uint_fast32_t s_kasan_error_count;
#endif
static inline bool kasan_checks_are_disabled(void)
{
return atomic_load_explicit(&s_kasan_suppress_depth, memory_order_relaxed) != 0;
}
static inline void kasan_report_enter(void)
{
atomic_fetch_add_explicit(&s_kasan_suppress_depth, 1, memory_order_relaxed);
}
static inline void kasan_report_exit(void)
{
atomic_fetch_sub_explicit(&s_kasan_suppress_depth, 1, memory_order_relaxed);
}
#if CONFIG_KASAN_NO_HALT
static inline uint32_t kasan_error_count_get(void)
{
return (uint32_t)atomic_load_explicit(&s_kasan_error_count, memory_order_relaxed);
}
static inline void kasan_error_count_reset(void)
{
atomic_store_explicit(&s_kasan_error_count, 0, memory_order_relaxed);
}
static inline void kasan_error_count_inc(void)
{
atomic_fetch_add_explicit(&s_kasan_error_count, 1, memory_order_relaxed);
}
#endif
/* ---- Shadow accessors --------------------------------------------------- */
/*
* These helpers are always inlined into their callers, so they carry no
* placement attribute of their own: when inlined into a flash-resident API
* (e.g. kasan_poison_region) they stay in flash, and when inlined into the
* IRAM hot path (kasan_is_valid_access / the __asan_* stubs) they run from
* IRAM with the rest of that function.
*/
static inline __attribute__((always_inline)) uint8_t *kasan_mem_to_shadow(uintptr_t addr)
{
return (uint8_t *)(kasan_shadow_offset + (addr >> 3));
}
static inline __attribute__((always_inline)) int kasan_is_high_nibble(uintptr_t addr)
{
return (addr >> 2) & 1;
}
static inline __attribute__((always_inline)) uint8_t kasan_get_nibble(uintptr_t addr)
{
uint8_t *shadow = kasan_mem_to_shadow(addr);
if (kasan_is_high_nibble(addr)) {
return (*shadow >> 4) & 0xF;
} else {
return *shadow & 0xF;
}
}
static inline __attribute__((always_inline)) void kasan_set_nibble(uintptr_t addr, uint8_t val)
{
uint8_t *shadow = kasan_mem_to_shadow(addr);
if (kasan_is_high_nibble(addr)) {
*shadow = (*shadow & 0x0F) | ((val & 0xF) << 4);
} else {
*shadow = (*shadow & 0xF0) | (val & 0xF);
}
}
static inline __attribute__((always_inline)) bool kasan_addr_in_shadow_range(uintptr_t addr)
{
uintptr_t map_base = KASAN_SHADOW_MAP_BASE;
uintptr_t map_end = map_base + ((uintptr_t)KASAN_SHADOW_SIZE << 3);
return (addr >= map_base && addr < map_end);
}
/* ---- Poison / unpoison -------------------------------------------------- */
void kasan_poison_region(const void *addr, size_t size, uint8_t tag)
{
if (!kasan_shadow_offset || size == 0) {
return;
}
uintptr_t start = (uintptr_t)addr;
uintptr_t end = start + size;
uintptr_t map_base = KASAN_SHADOW_MAP_BASE;
uintptr_t map_end = map_base + ((uintptr_t)KASAN_SHADOW_SIZE << 3);
if (end <= map_base || start >= map_end) {
return;
}
if (start < map_base) {
start = map_base;
}
if (end > map_end) {
end = map_end;
}
uintptr_t aligned_start = (start + 3) & ~3UL;
uintptr_t aligned_end = end & ~3UL;
if (start < aligned_start && start < end) {
kasan_set_nibble(start & ~3UL, tag);
}
for (uintptr_t a = aligned_start; a < aligned_end; a += 4) {
if ((a & 4) == 0 && (a + 4) < aligned_end) {
uint8_t *shadow = kasan_mem_to_shadow(a);
*shadow = (tag << 4) | tag;
a += 4;
} else {
kasan_set_nibble(a, tag);
}
}
if (aligned_end < end) {
kasan_set_nibble(aligned_end, tag);
}
}
void kasan_unpoison_region(const void *addr, size_t size)
{
if (!kasan_shadow_offset || size == 0) {
return;
}
uintptr_t start = (uintptr_t)addr;
uintptr_t end = start + size;
uintptr_t map_base = KASAN_SHADOW_MAP_BASE;
uintptr_t map_end = map_base + ((uintptr_t)KASAN_SHADOW_SIZE << 3);
if (end <= map_base || start >= map_end) {
return;
}
if (start < map_base) {
start = map_base;
}
if (end > map_end) {
end = map_end;
}
uintptr_t granule_start = start & ~3UL;
uintptr_t granule_end = (end + 3) & ~3UL;
for (uintptr_t a = granule_start; a < granule_end; a += 4) {
if (a + 4 > end && end > a) {
uint8_t partial = (uint8_t)(end - a);
if (partial > 0 && partial < 4) {
kasan_set_nibble(a, partial);
} else {
kasan_set_nibble(a, KASAN_NIBBLE_VALID);
}
} else {
if ((a & 4) == 0 && (a + 4) < granule_end) {
uint8_t *shadow = kasan_mem_to_shadow(a);
*shadow = 0x00;
a += 4;
} else {
kasan_set_nibble(a, KASAN_NIBBLE_VALID);
}
}
}
}
/* ---- Shadow initialisation ---------------------------------------------- */
void kasan_disable_checks(void)
{
atomic_fetch_add_explicit(&s_kasan_suppress_depth, 1, memory_order_acquire);
}
void kasan_enable_checks(void)
{
atomic_fetch_sub_explicit(&s_kasan_suppress_depth, 1, memory_order_release);
}
void kasan_init_shadow(void)
{
/*
* The shadow array lives in .data (DRAM_ATTR), not .bss, so it is loaded
* from the image rather than implicitly zeroed at startup. Zero it here
* explicitly so every nibble starts as 0 (= valid). The alloc hook then
* marks redzones and the free hook poisons freed blocks; no bulk poisoning
* is done here so boot-path code sees clean shadow and no false positives.
*/
memset(kasan_shadow_mem, 0, KASAN_SHADOW_SIZE);
kasan_shadow_offset = (uintptr_t)kasan_shadow_mem
- (KASAN_SHADOW_MAP_BASE >> 3);
esp_rom_printf("KASAN: kernel-address sanitizer initialized (shadow %u bytes, map base 0x%08" PRIxPTR ")\n",
(unsigned)KASAN_SHADOW_SIZE, (uintptr_t)KASAN_SHADOW_MAP_BASE);
}
/* ---- Error counter (CONFIG_KASAN_NO_HALT) ------------------------------- */
#if CONFIG_KASAN_NO_HALT
uint32_t kasan_get_error_count(void)
{
return kasan_error_count_get();
}
void kasan_reset_error_count(void)
{
kasan_error_count_reset();
}
#endif
/* ---- Access validation -------------------------------------------------- */
static inline __attribute__((always_inline)) bool kasan_is_valid_access(uintptr_t addr, size_t size)
{
/* Address outside monitored DRAM window, not mapped to shadow region, assume valid to avoid false positives */
if (!kasan_addr_in_shadow_range(addr) || !kasan_addr_in_shadow_range(addr + size - 1)) {
return true;
}
/*
* Fast path: both nibbles in the shadow byte are valid.
*
* Each shadow byte covers 8 real bytes (two 4-byte granules), so we can
* only short-circuit when the *entire* access falls inside the shadow
* byte(s) we have actually examined. Larger or mis-aligned accesses fall
* through to the slow path below, which walks every granule.
*/
uintptr_t offset_in_byte = addr & 7U;
uint8_t shadow_byte = *kasan_mem_to_shadow(addr);
if (shadow_byte == 0x00) {
if ((offset_in_byte + size) <= 8U) {
return true;
}
if ((offset_in_byte + size) <= 16U) {
uint8_t next_shadow = *kasan_mem_to_shadow(addr + 8);
if (next_shadow == 0x00) {
return true;
}
}
}
/* Slow path: check each granule. */
uintptr_t end_addr = addr + size;
for (uintptr_t a = addr; a < end_addr;) {
uint8_t nibble = kasan_get_nibble(a);
if (nibble == KASAN_NIBBLE_VALID) {
a = (a & ~3UL) + 4;
continue;
}
if (nibble >= 1 && nibble <= 3) {
uintptr_t granule_base = a & ~3UL;
uintptr_t offset_in_granule = a - granule_base;
uintptr_t access_end_in_granule = end_addr - granule_base;
if (access_end_in_granule > 4) {
access_end_in_granule = 4;
}
if (offset_in_granule >= nibble || access_end_in_granule > nibble) {
return false;
}
a = granule_base + 4;
continue;
}
return false;
}
return true;
}
/* ---- Error reporting ---------------------------------------------------- */
/*
* Bug-type and access-type strings live in DRAM so that the IRAM error
* reporting path stays safe when the SPI flash cache is disabled (ISR
* context, spi_flash operations, early boot). Plain string literals would
* land in .rodata (flash) and dereferencing them with cache off would mask
* the real KASAN violation with a cache-error panic.
*/
static DRAM_ATTR const char kasan_str_use_after_free[] = "use-after-free";
static DRAM_ATTR const char kasan_str_underflow_lrz[] = "heap-buffer-underflow (left redzone)";
static DRAM_ATTR const char kasan_str_overflow_rrz[] = "heap-buffer-overflow (right redzone)";
static DRAM_ATTR const char kasan_str_uninitialised[] = "uninitialised memory";
static DRAM_ATTR const char kasan_str_overflow_partial[] = "heap-buffer-overflow (partial granule)";
static DRAM_ATTR const char kasan_str_unknown_poison[] = "unknown poison";
static DRAM_ATTR const char kasan_str_write[] = "WRITE";
static DRAM_ATTR const char kasan_str_read[] = "READ";
#if !CONFIG_KASAN_NO_HALT
static DRAM_ATTR const char kasan_str_abort_msg[] = "KASAN: invalid memory access";
#endif
static DRAM_ATTR const char kasan_fmt_error[] = "KASAN error: %s of size %u at 0x%08x\n";
static DRAM_ATTR const char kasan_fmt_bug[] = " Bug type: %s (shadow nibble=0x%x)\n";
static IRAM_ATTR const char *kasan_nibble_to_string(uint8_t nibble)
{
switch (nibble) {
case KASAN_NIBBLE_HEAP_FREE: return kasan_str_use_after_free;
case KASAN_NIBBLE_HEAP_LRZ: return kasan_str_underflow_lrz;
case KASAN_NIBBLE_HEAP_RRZ: return kasan_str_overflow_rrz;
case KASAN_NIBBLE_UNINIT: return kasan_str_uninitialised;
default:
if (nibble >= 1 && nibble <= 3) {
return kasan_str_overflow_partial;
}
return kasan_str_unknown_poison;
}
}
static IRAM_ATTR void kasan_report(uintptr_t addr, size_t size, bool is_write)
{
kasan_report_enter();
if (esp_cpu_dbgr_is_attached()) {
esp_cpu_dbgr_break();
}
const char *access_type = is_write ? kasan_str_write : kasan_str_read;
uint8_t nibble = kasan_get_nibble(addr);
const char *bug_type = kasan_nibble_to_string(nibble);
esp_rom_printf(kasan_fmt_error, access_type, (unsigned)size, (unsigned)addr);
esp_rom_printf(kasan_fmt_bug, bug_type, nibble);
#if CONFIG_KASAN_NO_HALT
kasan_error_count_inc();
kasan_report_exit();
#else
/*
* Avoid flash-resident helpers (strlcat, libc string operations) here:
* kasan_report runs from IRAM and may execute with the SPI flash cache
* disabled. esp_rom_printf above has already emitted the detailed
* diagnostic via ROM; pass a short DRAM-resident message to the abort
* path so the panic itself does not touch flash.
*/
esp_system_abort(kasan_str_abort_msg);
#endif
}
/* ---- Check dispatcher --------------------------------------------------- */
static IRAM_ATTR void kasan_check(uintptr_t addr, size_t size, bool is_write)
{
if (__builtin_expect(kasan_checks_are_disabled() || !kasan_shadow_offset, 0)) {
return;
}
if (!kasan_is_valid_access(addr, size)) {
kasan_report(addr, size, is_write);
}
}
/* ---- GCC-emitted KASAN stubs -------------------------------------------- */
/*
* __attribute__((used)) prevents --gc-sections from removing stubs that GCC's
* instrumentation pass calls but that the linker cannot see at analysis time.
*/
__attribute__((used)) void IRAM_ATTR __asan_load1_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 1, false);
}
__attribute__((used)) void IRAM_ATTR __asan_load2_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 2, false);
}
__attribute__((used)) void IRAM_ATTR __asan_load4_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 4, false);
}
__attribute__((used)) void IRAM_ATTR __asan_load8_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 8, false);
}
__attribute__((used)) void IRAM_ATTR __asan_load16_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 16, false);
}
__attribute__((used)) void IRAM_ATTR __asan_loadN_noabort(void *addr, size_t size)
{
kasan_check((uintptr_t)addr, size, false);
}
__attribute__((used)) void IRAM_ATTR __asan_store1_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 1, true);
}
__attribute__((used)) void IRAM_ATTR __asan_store2_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 2, true);
}
__attribute__((used)) void IRAM_ATTR __asan_store4_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 4, true);
}
__attribute__((used)) void IRAM_ATTR __asan_store8_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 8, true);
}
__attribute__((used)) void IRAM_ATTR __asan_store16_noabort(void *addr)
{
kasan_check((uintptr_t)addr, 16, true);
}
__attribute__((used)) void IRAM_ATTR __asan_storeN_noabort(void *addr, size_t size)
{
kasan_check((uintptr_t)addr, size, true);
}
/* Called before noreturn functions; nothing to do on bare-metal. */
void IRAM_ATTR __asan_handle_no_return(void)
{
}
#endif /* CONFIG_COMPILER_KASAN */