Merge branch 'feature/add_kasan_support' into 'master'

feat(kasan): add Kernel Address Sanitizer (KASAN) support for ESP-IDF

Closes IDF-13467

See merge request espressif/esp-idf!48106
This commit is contained in:
Meet Patel
2026-06-25 11:30:48 +05:30
25 changed files with 1856 additions and 49 deletions
+45 -5
View File
@@ -56,6 +56,10 @@ else()
"system_time.c"
"stack_check.c"
"ubsan.c")
if(CONFIG_COMPILER_KASAN)
list(APPEND srcs "kasan.c")
endif()
if(CONFIG_SOC_WDT_SUPPORTED)
list(APPEND srcs "int_wdt.c")
endif()
@@ -110,11 +114,30 @@ else()
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u start_app_other_cores")
endif()
# Disable stack protection in files which are involved in initialization of that feature
set_source_files_properties(
"startup.c" "stack_check.c" "port/cpu_start.c"
PROPERTIES COMPILE_FLAGS
-fno-stack-protector)
if(CONFIG_COMPILER_KASAN)
# Files involved in stack-protector initialisation: disable stack protector.
# Also exclude from KASAN: cpu_start.c runs before kasan_init_shadow() and
# panic.c / startup.c must not recurse into KASAN during crash handling.
set_source_files_properties(
"startup.c" "stack_check.c" "port/cpu_start.c"
PROPERTIES COMPILE_FLAGS
"-fno-stack-protector -fno-sanitize=kernel-address")
# kasan.c and ubsan.c implement sanitizer runtime stubs – must never be
# instrumented themselves (infinite recursion).
# panic.c / port/panic_handler.c must not be instrumented to avoid
# recursion in the error path.
set_source_files_properties(
"kasan.c" "ubsan.c" "panic.c" "port/panic_handler.c"
PROPERTIES COMPILE_FLAGS
"-fno-sanitize=kernel-address")
else()
# Disable stack protection in files which are involved in initialization of that feature
set_source_files_properties(
"startup.c" "stack_check.c" "port/cpu_start.c"
PROPERTIES COMPILE_FLAGS
-fno-stack-protector)
endif()
target_linker_script(${COMPONENT_LIB} INTERFACE "ld/${target}/memory.ld.in")
@@ -137,6 +160,23 @@ endif()
# due to -ffunction-sections -Wl,--gc-sections options.
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u __ubsan_include")
# Force-link the __asan_*_noabort stubs: GCC-instrumented code calls them but
# the linker cannot see the call sites at GC time, so without explicit -u flags
# they would be silently dropped (and any -u flag against the file is enough
# to pull kasan.c in as well).
if(CONFIG_COMPILER_KASAN)
foreach(__kasan_stub
__asan_load1_noabort __asan_load2_noabort
__asan_load4_noabort __asan_load8_noabort
__asan_load16_noabort __asan_loadN_noabort
__asan_store1_noabort __asan_store2_noabort
__asan_store4_noabort __asan_store8_noabort
__asan_store16_noabort __asan_storeN_noabort
__asan_handle_no_return)
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u ${__kasan_stub}")
endforeach()
endif()
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_system_include_startup_funcs")
# [refactor-todo] requirements due to init code, should be removable
+91
View File
@@ -0,0 +1,91 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief KASAN shadow nibble poison tags.
*
* Each 4-bit nibble in the shadow covers a 4-byte granule of real memory.
* Values 0x0-0x3 indicate valid (or partially valid) memory; 0xC-0xF
* indicate poisoned memory with the tag describing the reason.
*/
#define KASAN_POISON_HEAP_FREE ((uint8_t)0xF) /**< Freed heap region */
#define KASAN_POISON_HEAP_LRZ ((uint8_t)0xE) /**< Heap left redzone (before alloc) */
#define KASAN_POISON_HEAP_RRZ ((uint8_t)0xD) /**< Heap right redzone (after alloc) */
#define KASAN_POISON_UNINIT ((uint8_t)0xC) /**< Never-allocated / uninitialised */
#if CONFIG_COMPILER_KASAN
/**
* @brief Initialise KASAN shadow memory.
*
* Must be called before heap_caps_init() so that the shadow region is ready
* when the first allocation hook fires.
*/
void kasan_init_shadow(void);
/**
* @brief Poison a memory region in the KASAN shadow.
*
* Marks [addr, addr+size) as invalid with the given @p tag.
*/
void kasan_poison_region(const void *addr, size_t size, uint8_t tag);
/**
* @brief Unpoison a memory region in the KASAN shadow.
*
* Marks [addr, addr+size) as valid (accessible).
*/
void kasan_unpoison_region(const void *addr, size_t size);
/**
* @brief Temporarily disable KASAN load/store checks on the current core.
*
* Increments a nested suppression counter; while it is non-zero, the
* __asan_load_N / __asan_store_N runtime stubs return without touching shadow
* memory. Each call must be paired with kasan_enable_checks().
*
* Intended for short critical sections that manipulate cache/MMU state or
* otherwise execute in conditions where accessing the KASAN shadow region
* would itself fault (for example, the early SPI flash chip probe in
* esp_flash_init_default_chip()).
*
* This API is safe to call from any context (task, ISR, panic handler).
*/
void kasan_disable_checks(void);
/**
* @brief Re-enable KASAN load/store checks suppressed by kasan_disable_checks().
*
* Decrements the suppression counter. Calls must be balanced; otherwise
* checks remain disabled (or, if unbalanced the other way, the counter wraps
* around and produces undefined behaviour).
*/
void kasan_enable_checks(void);
#endif
#if CONFIG_KASAN_NO_HALT
/**
* @brief Return the number of KASAN errors reported since boot or last reset.
*/
uint32_t kasan_get_error_count(void);
/**
* @brief Reset the KASAN error counter to zero.
*/
void kasan_reset_error_count(void);
#endif
#ifdef __cplusplus
}
#endif
+512
View File
@@ -0,0 +1,512 @@
/*
* 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 */
+12 -9
View File
@@ -401,6 +401,18 @@ void IRAM_ATTR do_multicore_settings(void)
FORCE_INLINE_ATTR IRAM_ATTR void init_cpu(void)
{
#ifdef __riscv
// Configure the global pointer register.
// This must be the first thing the IDF app does on RISC-V, as any other
// piece of code could be relaxed by the linker to access something relative
// to __global_pointer$. With KASAN enabled, even calls like
// esp_cpu_dbgr_is_attached() are instrumented and may emit gp-relative
// loads, so gp must be set up before any C function call.
__asm__ __volatile__(
".option push\n"
".option norelax\n"
"la gp, __global_pointer$\n"
".option pop"
);
if (esp_cpu_dbgr_is_attached()) {
/* Let debugger some time to detect that target started, halt it, enable ebreaks and resume.
500ms should be enough. */
@@ -408,15 +420,6 @@ FORCE_INLINE_ATTR IRAM_ATTR void init_cpu(void)
esp_rom_delay_us(100000);
}
}
// Configure the global pointer register
// (This should be the first thing IDF app does, as any other piece of code could be
// relaxed by the linker to access something relative to __global_pointer$)
__asm__ __volatile__(
".option push\n"
".option norelax\n"
"la gp, __global_pointer$\n"
".option pop"
);
#endif
/* NOTE: When ESP-TEE is enabled, this sets up the callback function
@@ -27,6 +27,10 @@
#include "esp_private/panic_internal.h"
#include "esp_private/panic_reason.h"
#if CONFIG_COMPILER_KASAN
#include "esp_kasan.h"
#endif
#if SOC_WDT_SUPPORTED || SOC_RTC_WDT_SUPPORTED
#include "hal/wdt_types.h"
#include "hal/wdt_hal.h"
@@ -128,6 +132,13 @@ void busy_wait(void)
static void panic_handler(void *frame, bool pseudo_excause)
{
#if CONFIG_COMPILER_KASAN
/* Disable KASAN checks for the remainder of crash handling: backtrace and
* stack dumps legitimately read guard pages and poisoned redzones, which
* would otherwise trigger spurious KASAN reports. */
kasan_disable_checks();
#endif
/* If watchdogs are enabled, the panic handler runs the risk of getting aborted pre-emptively because
* an overzealous watchdog decides to reset it. Hence, we feed the WDTs here.
*
+4
View File
@@ -24,6 +24,10 @@ CORE: 10: init_show_cpu_freq in components/esp_system/startup_funcs.c on BIT(0)
CORE: 20: init_show_app_info in components/esp_app_format/esp_app_desc.c on BIT(0)
CORE: 21: init_efuse_show_app_info in components/efuse/src/esp_efuse_startup.c on BIT(0)
# When KASAN is enabled, initialise the KASAN shadow region before the heap allocator.
# The shadow offset must be set up before the first heap_caps_init hook fires.
CORE: 98: init_kasan_shadow in components/heap/heap_kasan.c on BIT(0)
# Set the standard stream to non blocking and register the default vfs
CORE: 99: init_vfs_linux_coop in components/vfs/vfs_linux_default_coop.c on BIT(0)
+39 -2
View File
@@ -66,11 +66,13 @@ if(NOT BOOTLOADER_BUILD)
endif()
endif()
set(ldfragments linker.lf)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS ${includes}
PRIV_INCLUDE_DIRS ${priv_includes}
LDFRAGMENTS linker.lf
PRIV_REQUIRES soc)
LDFRAGMENTS ${ldfragments}
PRIV_REQUIRES soc esp_system)
if(CONFIG_HEAP_TLSF_USE_ROM_IMPL AND NOT BOOTLOADER_BUILD)
# After registering the component, set the tlsf_set_rom_patches symbol as undefined
@@ -110,3 +112,38 @@ else()
endif()
endif()
endif()
# KASAN heap integration: hook into alloc/free to maintain shadow memory.
# heap_kasan.c holds the implementation (compiled without KASAN instrumentation).
# heap_kasan_hooks.c provides strong (non-weak) overrides of the hook stubs;
# it intentionally avoids including esp_heap_caps.h to prevent GCC from
# inheriting the 'weak' attribute from the declarations in that header.
if(CONFIG_COMPILER_KASAN AND CONFIG_HEAP_USE_HOOKS)
target_sources(${COMPONENT_LIB} PRIVATE
"heap_kasan.c"
"heap_kasan_hooks.c"
)
# Sources excluded from KASAN instrumentation:
# heap_kasan.c / heap_kasan_hooks.c implement the sanitizer hooks themselves
# (instrumenting them would cause infinite recursion).
# heap_caps_init.c runs while heap metadata is being brought up: the first
# allocations happen before the shadow is fully initialised, and the
# memcpy of the registered-heaps array touches DRAM regions whose
# shadow state is still being populated.
set_source_files_properties(
"heap_caps_init.c"
"heap_kasan.c"
"heap_kasan_hooks.c"
PROPERTIES COMPILE_OPTIONS "-fno-sanitize=kernel-address"
)
idf_component_get_property(esp_system_lib esp_system COMPONENT_LIB)
target_link_libraries(${COMPONENT_LIB} PRIVATE ${esp_system_lib})
# Force the linker to include our strong hook definitions. Without this,
# --gc-sections would silently discard them because the call site in
# heap_caps_base.c uses a weak-symbol pointer (if (hook != NULL) ...) which
# doesn't create a strong reference that the linker follows.
target_link_libraries(${COMPONENT_LIB} INTERFACE
"-u esp_heap_trace_alloc_hook"
"-u esp_heap_trace_free_hook"
)
endif()
+11 -1
View File
@@ -13,6 +13,7 @@
#include "multi_heap.h"
#include "esp_log.h"
#include "heap_private.h"
#include "heap_kasan_layout.h"
#include "esp_system.h"
/*
@@ -480,13 +481,22 @@ void heap_caps_dump_all(void)
size_t heap_caps_get_allocated_size( void *ptr )
{
/* The heap layout is:
* [block-owner][left redzone][user][right redzone]
* so undo the KASAN shift before removing the block-owner word.
*/
ptr = KASAN_USER_TO_PTR(ptr);
// add the block owner bytes back to ptr before handing over
// to multi heap layer.
ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(ptr);
heap_t *heap = find_containing_heap(ptr);
assert(heap);
size_t size = multi_heap_get_allocated_size(heap->heap, ptr);
return MULTI_HEAP_REMOVE_BLOCK_OWNER_SIZE(size);
size = MULTI_HEAP_REMOVE_BLOCK_OWNER_SIZE(size);
if (size > 2 * KASAN_RZ) {
size -= 2 * KASAN_RZ;
}
return size;
}
size_t heap_caps_get_containing_block_size(void *ptr)
+105 -30
View File
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -12,6 +12,7 @@
#include "multi_heap.h"
#include "esp_log.h"
#include "heap_private.h"
#include "heap_kasan_layout.h"
#if CONFIG_HEAP_TASK_TRACKING
#include "esp_heap_task_info.h"
#include "esp_heap_task_info_internal.h"
@@ -28,9 +29,28 @@
#define CALL_HOOK(hook, ...) {}
#endif
/*
* KASAN redzone support: inflate allocations by 2*KASAN_RZ bytes and shift
* the user pointer past the left redzone. heap_kasan.c poisons the redzones.
*
* Final allocation layout (with CONFIG_HEAP_TASK_TRACKING=y):
*
* [ block-owner word ][ left redzone ][ user bytes ][ right redzone ]
*
* The ordering is intentional: user underflows must hit the left redzone
* before they can reach the task-tracking block-owner word.
*
* Pointer arithmetic macros live in heap_kasan_layout.h.
*/
//This is normally provided by the heap-memalign-hw component.
extern void esp_heap_adjust_alignment_to_hw(size_t *p_alignment, size_t *p_size, uint32_t *p_caps);
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
bool kasan_heap_should_defer_free(void);
void kasan_heap_clear_deferred_free(void);
#endif
//Default alignment the multiheap allocator / tlsf will align 'unaligned' memory to, in bytes
#define UNALIGNED_MEM_ALIGNMENT_BYTES 4
@@ -67,6 +87,17 @@ HEAP_IRAM_ATTR void heap_caps_free( void *ptr)
return;
}
/*
* KASAN free hook must see the same pointer that the alloc hook saw, i.e.
* the user-visible (IRAM) pointer with the redzone shift applied. Call
* it before any DIRAM -> DRAM translation; otherwise the shadow update
* would touch the wrong address range and use-after-free detection on
* IRAM-aliased blocks would be incorrect.
*/
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
CALL_HOOK(esp_heap_trace_free_hook, ptr);
#endif
if ((!esp_dram_match_iram() && esp_ptr_in_diram_iram(ptr)) ||
(!esp_rtc_dram_match_rtc_iram() && esp_ptr_in_rtc_iram_fast(ptr))) {
//Memory allocated here is actually allocated in the DRAM alias region and
@@ -75,17 +106,29 @@ HEAP_IRAM_ATTR void heap_caps_free( void *ptr)
uint32_t *dramAddrPtr = (uint32_t *)ptr;
ptr = (void *)dramAddrPtr[-1];
}
void *block_owner_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(ptr);
/* Reverse the pointer transforms in the opposite order used on alloc:
* user -> left redzone start -> block-owner word.
*/
void *raw_ptr = KASAN_USER_TO_PTR(ptr);
void *block_owner_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(raw_ptr);
heap_t *heap = find_containing_heap(block_owner_ptr);
assert(heap != NULL && "free() target pointer is outside heap areas");
#if CONFIG_HEAP_TASK_TRACKING
heap_caps_update_per_task_info_free(heap, ptr);
heap_caps_update_per_task_info_free(heap, raw_ptr);
#endif
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
if (kasan_heap_should_defer_free()) {
kasan_heap_clear_deferred_free();
return;
}
multi_heap_free(heap->heap, block_owner_ptr);
#else
multi_heap_free(heap->heap, block_owner_ptr);
CALL_HOOK(esp_heap_trace_free_hook, ptr);
#endif
}
HEAP_IRAM_ATTR static inline void *aligned_or_unaligned_alloc(multi_heap_handle_t heap, size_t size, size_t alignment, size_t offset) {
@@ -139,6 +182,8 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment
size = (size + 3) & (~3); // int overflow checked above
}
const size_t alloc_size = KASAN_ADD_RZ(size);
for (int prio = 0; prio < SOC_MEMORY_TYPE_NO_PRIOS; prio++) {
//Iterate over heaps and check capabilities at this priority
heap_t *heap;
@@ -159,7 +204,7 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment
//This is special, insofar that what we're going to get back is a DRAM address. If so,
//we need to 'invert' it (lowest address in DRAM == highest address in IRAM and vice-versa) and
//add a pointer to the DRAM equivalent before the address we're going to return.
ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(size) + 4,
ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(alloc_size) + 4,
alignment, MULTI_HEAP_BLOCK_OWNER_SIZE()); // int overflow checked above
if (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING
@@ -171,13 +216,14 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment
MULTI_HEAP_SET_BLOCK_OWNER(ret);
ret = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ret);
ret = KASAN_PTR_TO_USER(ret);
uint32_t *iptr = dram_alloc_to_iram_addr(ret, size + 4); // int overflow checked above
CALL_HOOK(esp_heap_trace_alloc_hook, iptr, size, caps);
return iptr;
}
} else {
//Just try to alloc, nothing special.
ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(size),
ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(alloc_size),
alignment, MULTI_HEAP_BLOCK_OWNER_SIZE());
if (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING
@@ -189,6 +235,7 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment
MULTI_HEAP_SET_BLOCK_OWNER(ret);
ret = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ret);
ret = KASAN_PTR_TO_USER(ret);
CALL_HOOK(esp_heap_trace_alloc_hook, ret, size, caps);
return ret;
}
@@ -236,31 +283,43 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_realloc_base( void *ptr, size_t siz
return NULL;
}
//The pointer to memory may be aliased, we need to
//recover the corresponding address before to manage a new allocation:
if(esp_ptr_in_diram_iram((void *)ptr)) {
uint32_t *dram_addr = (uint32_t *)ptr;
dram_ptr = (void *)dram_addr[-1];
/*
* DIRAM aliasing detection must happen on the original user pointer:
* dram_alloc_to_iram_addr stores the underlying DRAM address one word
* before the IRAM pointer, so the KASAN redzone shift (which moves the
* pointer by KASAN_RZ on the IRAM side) would land us in the wrong place
* if we applied it first.
*/
void *raw_ptr;
if (esp_ptr_in_diram_iram(ptr)) {
uint32_t *iram_addr = (uint32_t *)ptr;
dram_ptr = (void *)iram_addr[-1];
/* On the DRAM side the layout is [block-owner][left redzone][user]
* so undo redzone first, then block-owner, matching alloc order. */
dram_ptr = KASAN_USER_TO_PTR(dram_ptr);
dram_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(dram_ptr);
heap = find_containing_heap(dram_ptr);
assert(heap != NULL && "realloc() pointer is outside heap areas");
//with pointers that reside on diram space, we avoid using
//the realloc implementation due to address translation issues,
//instead force a malloc/copy/free
/* with pointers that reside on diram space, we avoid using
* the realloc implementation due to address translation issues,
* instead force a malloc/copy/free */
ptr_in_diram_case = true;
raw_ptr = NULL; /* not used in the diram path */
} else {
heap = find_containing_heap(ptr);
/* Reverse the alloc-time layout transform in the same order as free():
* user -> left redzone start -> block-owner word. Doing the
* block-owner removal *before* find_containing_heap() matches the
* free() path and the DIRAM branch above. */
raw_ptr = KASAN_USER_TO_PTR(ptr);
raw_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(raw_ptr);
heap = find_containing_heap(raw_ptr);
assert(heap != NULL && "realloc() pointer is outside heap areas");
}
// shift ptr by block owner offset. Since the ptr returned to the user
// does not include the block owner bytes (that are located at the
// beginning of the allocated memory) we have to add them back before
// processing the realloc.
ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(ptr);
const size_t alloc_size = KASAN_ADD_RZ(size);
// are the existing heap's capabilities compatible with the
// requested ones?
@@ -273,17 +332,17 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_realloc_base( void *ptr, size_t siz
// (which will resize the block if it can)
#if CONFIG_HEAP_TASK_TRACKING
size_t old_size = multi_heap_get_full_block_size(heap->heap, ptr);
TaskHandle_t old_task = MULTI_HEAP_GET_BLOCK_OWNER(ptr);
size_t old_size = multi_heap_get_full_block_size(heap->heap, raw_ptr);
TaskHandle_t old_task = MULTI_HEAP_GET_BLOCK_OWNER(raw_ptr);
#endif
void *r = multi_heap_realloc(heap->heap, ptr, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(size));
void *r = multi_heap_realloc(heap->heap, raw_ptr, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(alloc_size));
if (r != NULL) {
MULTI_HEAP_SET_BLOCK_OWNER(r);
#if CONFIG_HEAP_TASK_TRACKING
heap_caps_update_per_task_info_realloc(heap,
MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ptr),
MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(raw_ptr),
MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(r),
old_size, old_task,
multi_heap_get_full_block_size(heap->heap, r),
@@ -291,6 +350,19 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_realloc_base( void *ptr, size_t siz
#endif
r = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(r);
r = KASAN_PTR_TO_USER(r);
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
/*
* If multi_heap_realloc() relocated the block (r != ptr), the
* old user range needs its shadow re-poisoned as use-after-free
* so that lingering references hit a KASAN violation. When the
* block was resized in place, alloc hook below will simply
* refresh the shadow over the new range.
*/
if (r != ptr) {
CALL_HOOK(esp_heap_trace_free_hook, ptr);
}
#endif
CALL_HOOK(esp_heap_trace_alloc_hook, r, size, caps);
return r;
}
@@ -307,14 +379,17 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_realloc_base( void *ptr, size_t siz
if(ptr_in_diram_case) {
old_size = multi_heap_get_allocated_size(heap->heap, dram_ptr);
} else {
old_size = multi_heap_get_allocated_size(heap->heap, ptr);
old_size = multi_heap_get_allocated_size(heap->heap, raw_ptr);
}
assert(old_size > 0);
// do not copy the block owner bytes
memcpy(new_p, MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ptr), MIN(size, old_size));
// add the block owner bytes to ptr since they are removed in heap_caps_free
heap_caps_free(MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ptr));
old_size = MULTI_HEAP_REMOVE_BLOCK_OWNER_SIZE(old_size);
/* Subtract KASAN redzone overhead to get the actual user-data size. */
if (old_size > 2 * KASAN_RZ) {
old_size -= 2 * KASAN_RZ;
}
memcpy(new_p, ptr, MIN(size, old_size));
heap_caps_free(ptr);
return new_p;
}
+288
View File
@@ -0,0 +1,288 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* KASAN heap hooks for ESP-IDF.
*
* Provides strong definitions of the weak heap hooks so the KASAN shadow stays
* in sync with every heap_caps_malloc / heap_caps_free.
*
* When CONFIG_KASAN_HEAP_REDZONE_SIZE > 0, each allocation gets a poisoned
* guard band on both sides (left and right redzones) for overflow/underflow
* detection.
*
* When CONFIG_KASAN_QUARANTINE_SIZE > 0, freed blocks are held in a FIFO
* before the real free, keeping their shadow poisoned to catch use-after-free.
*
* Compiled with -fno-sanitize=kernel-address to avoid recursive instrumentation.
*/
#include <assert.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "sdkconfig.h"
#include "esp_rom_sys.h"
/*
* Avoid including esp_heap_caps.h: it declares the hook symbols as weak, and
* GCC propagates that attribute to definitions in the same TU. Forward-declare
* only what we need.
*/
void heap_caps_free(void *ptr);
size_t heap_caps_get_allocated_size(void *ptr);
void kasan_heap_alloc_impl(void *ptr, size_t size, uint32_t caps);
void kasan_heap_free_impl(void *ptr);
bool kasan_heap_should_defer_free(void);
void kasan_heap_clear_deferred_free(void);
#include "esp_kasan.h"
#include "esp_private/startup_internal.h"
#include "heap_private.h"
#include "heap_kasan_layout.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
/*
* Initialise the KASAN shadow region before the heap allocator (priority 100).
* Runs at priority 99 so the shadow offset is in place before the first heap
* caps registration, and before the alloc/free hooks below start running.
*/
ESP_SYSTEM_INIT_FN(init_kasan_shadow, CORE, BIT(0), 98)
{
kasan_init_shadow();
return ESP_OK;
}
/* ---- Left-redzone header ------------------------------------------------ */
#define KASAN_LRZ_MAGIC 0xA5B6C7D8UL
typedef struct {
uint32_t magic;
size_t user_size;
} kasan_lrz_hdr_t;
#if HEAP_KASAN_RZ_ENABLED
_Static_assert((CONFIG_KASAN_HEAP_REDZONE_SIZE % 4) == 0,
"CONFIG_KASAN_HEAP_REDZONE_SIZE must be a multiple of 4");
_Static_assert(sizeof(kasan_lrz_hdr_t) <= KASAN_RZ,
"CONFIG_KASAN_HEAP_REDZONE_SIZE is too small to hold the KASAN header");
#endif
/* ---- Quarantine ring-buffer --------------------------------------------- */
static inline unsigned kasan_q_core_id(void)
{
int core_id = xPortGetCoreID();
assert(core_id >= 0 && core_id < portNUM_PROCESSORS);
return (unsigned)core_id;
}
#if CONFIG_KASAN_QUARANTINE_SIZE > 0
/*
* Per-core "deferred-free ticket counter". Each call into
* kasan_heap_free_impl() enqueues one block in the quarantine and bumps the
* counter; heap_caps_free() pops one ticket through kasan_heap_should_defer_free
* + kasan_heap_clear_deferred_free. A counter (rather than a bool) is required
* because frees can nest: e.g. heap_caps_free(A) starts on core 0, an ISR fires
* mid-way and runs heap_caps_free(B) on the same core, then heap_caps_free(A)
* resumes. With a bool the ISR's "clear" would mask the outer free's defer
* request and the outer pointer would be released both via multi_heap_free()
* *and* later via the quarantine eviction (double free).
*
* Access is from one core at a time but can be from an ISR on that core, so
* an atomic fetch-add is sufficient; we don't need a cross-core barrier here.
*/
static DRAM_ATTR atomic_uint_fast32_t s_q_defer_free[portNUM_PROCESSORS];
#define KASAN_Q_ENTRIES 64U
typedef struct {
void *ptr;
size_t size;
} kasan_q_entry_t;
static kasan_q_entry_t s_q[KASAN_Q_ENTRIES];
static unsigned s_q_head;
static unsigned s_q_tail;
static size_t s_q_bytes;
static portMUX_TYPE s_q_mux = portMUX_INITIALIZER_UNLOCKED;
static void kasan_q_release_one(void *ptr)
{
void *raw_ptr = KASAN_USER_TO_RAW(ptr);
void *block_owner_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(raw_ptr);
heap_t *heap = find_containing_heap(block_owner_ptr);
assert(heap != NULL && "quarantine free target pointer is outside heap areas");
multi_heap_free(heap->heap, block_owner_ptr);
}
static void kasan_q_add(void *ptr, size_t size)
{
/*
* Collect every block that needs to be evicted into a small on-stack
* array. We update head/tail/bytes atomically inside the critical
* section and only call multi_heap_free() after we have exited it. This
* avoids the previous "drop the lock, do work, re-acquire" pattern where
* a concurrent kasan_q_add() on the other core could mutate s_q_head /
* s_q_tail / s_q_bytes during the lock-release window and we would
* resume with stale state.
*/
void *evict[KASAN_Q_ENTRIES];
unsigned n_evict = 0;
portENTER_CRITICAL(&s_q_mux);
/* If the ring is full, evict the oldest entry to make room. */
unsigned next = (s_q_head + 1U) % KASAN_Q_ENTRIES;
if (next == s_q_tail) {
evict[n_evict++] = s_q[s_q_tail].ptr;
s_q_bytes -= s_q[s_q_tail].size;
s_q_tail = (s_q_tail + 1U) % KASAN_Q_ENTRIES;
}
/* Insert the new entry at head. */
s_q[s_q_head].ptr = ptr;
s_q[s_q_head].size = size;
s_q_head = (s_q_head + 1U) % KASAN_Q_ENTRIES;
s_q_bytes += size;
/* Trim back to the configured byte budget. */
while (s_q_bytes > (size_t)CONFIG_KASAN_QUARANTINE_SIZE
&& s_q_tail != s_q_head
&& n_evict < KASAN_Q_ENTRIES) {
evict[n_evict++] = s_q[s_q_tail].ptr;
s_q_bytes -= s_q[s_q_tail].size;
s_q_tail = (s_q_tail + 1U) % KASAN_Q_ENTRIES;
}
portEXIT_CRITICAL(&s_q_mux);
for (unsigned i = 0; i < n_evict; i++) {
kasan_q_release_one(evict[i]);
}
}
#endif /* CONFIG_KASAN_QUARANTINE_SIZE > 0 */
bool kasan_heap_should_defer_free(void)
{
#if CONFIG_KASAN_QUARANTINE_SIZE > 0
return atomic_load_explicit(&s_q_defer_free[kasan_q_core_id()],
memory_order_acquire) > 0U;
#else
return false;
#endif
}
void kasan_heap_clear_deferred_free(void)
{
#if CONFIG_KASAN_QUARANTINE_SIZE > 0
/*
* Consume one ticket. Wrapping below zero is treated as a programming
* error (call to clear() without matching enqueue from kasan_q_add).
*/
uint_fast32_t prev = atomic_fetch_sub_explicit(&s_q_defer_free[kasan_q_core_id()],
1U, memory_order_release);
assert(prev > 0U && "kasan_heap_clear_deferred_free: counter underflow");
(void)prev;
#endif
}
/* ---- Heap hooks --------------------------------------------------------- */
void kasan_heap_alloc_impl(void *ptr, size_t size, uint32_t caps)
{
(void)caps;
if (ptr == NULL || size == 0) {
return;
}
/*
* Mark the full granule containing the tail of the user allocation as
* unconditionally valid (instead of "first N bytes valid"). Many libc
* memcpy / strlen / strcpy implementations on RISC-V perform word-at-a-
* time loads and may legitimately touch the bytes between the requested
* end and the next 4-byte boundary; treating those as a partial poison
* would flag a false positive. The actual right redzone still starts at
* the next granule boundary, so genuine overflows past 4 bytes are still
* detected.
*/
const size_t granule_aligned_size = (size + 3U) & ~(size_t)3U;
kasan_unpoison_region(ptr, granule_aligned_size);
#if CONFIG_KASAN_HEAP_REDZONE_SIZE > 0
uint8_t *lrz = (uint8_t *)ptr - KASAN_RZ;
kasan_lrz_hdr_t hdr = { .magic = KASAN_LRZ_MAGIC, .user_size = size };
memcpy(lrz, &hdr, sizeof(hdr));
kasan_poison_region(lrz, KASAN_RZ, KASAN_POISON_HEAP_LRZ);
/* Start RRZ at the next granule boundary, not at user_ptr + size. */
uint8_t *rrz = (uint8_t *)ptr + granule_aligned_size;
kasan_poison_region(rrz, KASAN_RZ, KASAN_POISON_HEAP_RRZ);
#endif
}
void kasan_heap_free_impl(void *ptr)
{
if (ptr == NULL) {
return;
}
#if CONFIG_KASAN_HEAP_REDZONE_SIZE > 0
kasan_lrz_hdr_t hdr;
uint8_t *lrz = (uint8_t *)ptr - KASAN_RZ;
memcpy(&hdr, lrz, sizeof(hdr));
if (hdr.magic == KASAN_LRZ_MAGIC) {
size_t total = KASAN_RZ + hdr.user_size + KASAN_RZ;
kasan_poison_region(lrz, total, KASAN_POISON_HEAP_FREE);
} else {
size_t poison_size = heap_caps_get_allocated_size(ptr);
if (poison_size == 0) {
poison_size = 8;
}
kasan_poison_region(ptr, poison_size, KASAN_POISON_HEAP_FREE);
}
#else
size_t poison_size = heap_caps_get_allocated_size(ptr);
if (poison_size == 0) {
poison_size = 8;
}
kasan_poison_region(ptr, poison_size, KASAN_POISON_HEAP_FREE);
#endif
#if CONFIG_KASAN_QUARANTINE_SIZE > 0
size_t q_size = 8;
#if CONFIG_KASAN_HEAP_REDZONE_SIZE > 0
{
kasan_lrz_hdr_t qhdr;
memcpy(&qhdr, (uint8_t *)ptr - KASAN_RZ, sizeof(qhdr));
if (qhdr.magic == KASAN_LRZ_MAGIC) {
q_size = qhdr.user_size;
}
}
#endif
kasan_q_add(ptr, q_size);
/*
* Bump the per-core counter *after* the block is safely in the
* quarantine. This keeps in-progress heap_caps_free() invocations on
* the same core (including ISR-driven nested ones) in 1:1 lock-step with
* kasan_heap_clear_deferred_free() consumes, so neither the outer call
* nor the ISR-injected call can hand its pointer back to multi_heap_free
* after the block is already queued for deferred release.
*/
atomic_fetch_add_explicit(&s_q_defer_free[kasan_q_core_id()], 1U,
memory_order_release);
#endif
}
#endif /* CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS */
+48
View File
@@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Strong (non-weak) definitions of the heap trace hooks for KASAN.
*
* IMPORTANT: This file must NOT include esp_heap_caps.h or any header that
* transitively includes it (e.g. freertos/idf_additions.h). The reason is
* that esp_heap_caps.h declares esp_heap_trace_alloc_hook and
* esp_heap_trace_free_hook with __attribute__((weak)), and GCC propagates the
* weak attribute to the definition in the same TU. By keeping this file free
* of that header we get strong (globally overriding) definitions that the
* linker will prefer over the empty weak stubs.
*
* The actual KASAN logic lives in heap_kasan.c; this file just calls into it.
*/
#include "sdkconfig.h"
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS
#include <stddef.h>
#include <stdint.h>
#include "esp_kasan.h"
/* Forward-declare the real implementation from heap_kasan.c */
void kasan_heap_alloc_impl(void *ptr, size_t size, uint32_t caps);
void kasan_heap_free_impl(void *ptr);
/*
* These definitions are strong because this TU never sees the weak declaration
* from esp_heap_caps.h. The linker will therefore use these in preference to
* the empty weak stubs generated by the heap component's own code.
*/
void esp_heap_trace_alloc_hook(void *ptr, size_t size, uint32_t caps)
{
kasan_heap_alloc_impl(ptr, size, caps);
}
void esp_heap_trace_free_hook(void *ptr)
{
kasan_heap_free_impl(ptr);
}
#endif /* CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS */
+43
View File
@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "sdkconfig.h"
/*
* Shared KASAN heap redzone layout helpers for heap_caps_base.c, heap_caps.c,
* and heap_kasan.c.
*
* Allocation layout (with CONFIG_HEAP_TASK_TRACKING=y):
*
* [ block-owner word ][ left redzone ][ user bytes ][ right redzone ]
*
* The user-visible pointer points at the start of the user region.
* KASAN_USER_TO_PTR / KASAN_USER_TO_RAW move back to the left redzone start;
* KASAN_PTR_TO_USER moves a block-owner-relative pointer to the user region.
*/
#if CONFIG_COMPILER_KASAN && CONFIG_HEAP_USE_HOOKS && (CONFIG_KASAN_HEAP_REDZONE_SIZE > 0)
#define HEAP_KASAN_RZ_ENABLED 1
#else
#define HEAP_KASAN_RZ_ENABLED 0
#endif
#if HEAP_KASAN_RZ_ENABLED
#define KASAN_RZ CONFIG_KASAN_HEAP_REDZONE_SIZE
#define KASAN_ADD_RZ(sz) ((sz) + 2 * KASAN_RZ)
#define KASAN_PTR_TO_USER(p) ((void *)((uint8_t *)(p) + KASAN_RZ))
#define KASAN_USER_TO_PTR(p) ((void *)((uint8_t *)(p) - KASAN_RZ))
#define KASAN_USER_TO_RAW(p) KASAN_USER_TO_PTR(p)
#else
#define KASAN_RZ 0
#define KASAN_ADD_RZ(sz) (sz)
#define KASAN_PTR_TO_USER(p) (p)
#define KASAN_USER_TO_PTR(p) (p)
#define KASAN_USER_TO_RAW(p) (p)
#endif