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
+66
View File
@@ -207,6 +207,20 @@ elseif(CONFIG_COMPILER_STACK_CHECK_MODE_ALL)
list(APPEND compile_options "-fstack-protector-all") list(APPEND compile_options "-fstack-protector-all")
endif() endif()
if(CONFIG_COMPILER_KASAN)
# Only instrument the app build; the bootloader runs before kasan_init_shadow()
# is called and does not have the KASAN runtime.
if(NOT BOOTLOADER_BUILD)
list(APPEND c_compile_options "-fsanitize=kernel-address")
list(APPEND cxx_compile_options "-fsanitize=kernel-address")
if(NOT CONFIG_KASAN_STACK)
list(APPEND c_compile_options "--param" "asan-stack=0")
list(APPEND cxx_compile_options "--param" "asan-stack=0")
endif()
list(APPEND link_options "-fsanitize=kernel-address")
endif()
endif()
if(CONFIG_COMPILER_DUMP_RTL_FILES) if(CONFIG_COMPILER_DUMP_RTL_FILES)
list(APPEND compile_options "-fdump-rtl-expand") list(APPEND compile_options "-fdump-rtl-expand")
endif() endif()
@@ -377,3 +391,55 @@ if(NOT bootloader_build AND NOT esp_tee_build)
include("${CMAKE_CURRENT_LIST_DIR}/tools/cmake/component_validation.cmake") include("${CMAKE_CURRENT_LIST_DIR}/tools/cmake/component_validation.cmake")
__component_validation_run_checks() __component_validation_run_checks()
endif() endif()
# KASAN: exclude low-level / hardware-access components from instrumentation.
# Apply the exclusion after add_subdirectory() so every target already exists.
#
# Rationale per bucket:
# - hal / soc / esp_rom + every esp_hal_* peripheral HAL: perform volatile
# MMIO accesses (outside the shadow window, so each check is a no-op but
# still pays the indirect call into __asan_load* / __asan_store*).
# - spi_flash: runs with the flash cache disabled.
# - esp_hw_support: RTC/PMU register access, runs with MSPI bus held.
# - bootloader_support: runs before kasan_init_shadow().
# - freertos: scheduler / ISR plumbing is too hot to instrument.
# - heap: walks its own TLSF metadata living inside the (poisoned) pool;
# instrumented metadata walks would self-trigger. Shadow updates are
# handled explicitly via heap_kasan.c / heap_kasan_hooks.c, which are
# individually compiled with -fno-sanitize via set_source_files_properties.
if(CONFIG_COMPILER_KASAN AND NOT BOOTLOADER_BUILD)
# Match every esp_hal_* peripheral HAL component dynamically (instead of
# listing them one by one) so newly added HAL components stay excluded
# without revisiting this file.
idf_build_get_property(__kasan_all_components BUILD_COMPONENTS)
set(__kasan_excluded_components "")
foreach(__kasan_c ${__kasan_all_components})
if(__kasan_c MATCHES "^esp_hal_")
list(APPEND __kasan_excluded_components ${__kasan_c})
endif()
endforeach()
list(APPEND __kasan_excluded_components
hal soc esp_rom
spi_flash
esp_hw_support
bootloader_support
freertos
heap
)
foreach(__kasan_comp ${__kasan_excluded_components})
set(__kasan_lib "__idf_${__kasan_comp}")
if(TARGET ${__kasan_lib})
get_target_property(__kasan_type ${__kasan_lib} TYPE)
if(NOT __kasan_type STREQUAL "INTERFACE_LIBRARY")
# Only apply to real (non-INTERFACE) libraries that have source
# files. INTERFACE libraries have no sources so there is nothing
# to de-instrument, and adding an INTERFACE compile option would
# propagate -fno-sanitize to all downstream consumers (including
# the application under test).
target_compile_options(${__kasan_lib} PRIVATE "-fno-sanitize=kernel-address")
endif()
endif()
endforeach()
endif()
+78
View File
@@ -848,6 +848,83 @@ mainmenu "Espressif IoT Development Framework Configuration"
Define _GLIBCXX23_CONSTEXPR=__attribute__((cold)). Define _GLIBCXX23_CONSTEXPR=__attribute__((cold)).
endchoice endchoice
config COMPILER_KASAN
bool "Enable Kernel Address Sanitizer (KASAN)"
depends on IDF_EXPERIMENTAL_FEATURES && IDF_TOOLCHAIN_GCC && !IDF_TARGET_LINUX
select HEAP_USE_HOOKS
default n
help
Enables Kernel Address Sanitizer instrumentation (-fsanitize=kernel-address).
GCC instruments every memory load and store with calls to __asan_load<N>_noabort /
__asan_store<N>_noabort. These stubs check a shadow memory region and panic if
poisoned (freed / out-of-bounds) memory is accessed.
KASAN is useful for detecting:
- Heap buffer overflows and underflows (with redzones)
- Use-after-free bugs (when heap hooks are enabled)
- Out-of-bounds accesses in global and stack variables (optional)
Enabling this option increases code size by 1.5-3x for instrumented components
and reserves shadow memory in DRAM (~42-64 KiB depending on target).
NOT compatible with bootloader builds or ROM code. Components in the
hal, soc, esp_rom, and bootloader_support families are automatically
excluded from instrumentation.
menu "Kernel Address Sanitizer (KASAN)"
depends on COMPILER_KASAN
config KASAN_STACK
bool "Instrument stack variables (higher overhead)"
depends on COMPILER_KASAN
default n
help
Pass --param asan-stack=1 to enable KASAN instrumentation of
stack (local) variables. This detects stack buffer overflows but
significantly increases stack usage for every instrumented function.
Leave disabled unless you specifically need stack-overflow detection.
config KASAN_HEAP_REDZONE_SIZE
int "Heap allocation redzone size in bytes (0 to disable)"
depends on COMPILER_KASAN
default 8
range 0 64
help
Number of bytes of poisoned redzone added on each side of every heap
allocation. Redzones catch heap buffer overflows and underflows.
This value must be a multiple of 4; the build enforces that with
a static assertion in the heap KASAN runtime. Set to 0 to disable
redzones (reduces per-allocation overhead at the cost of reduced
detection coverage).
config KASAN_QUARANTINE_SIZE
int "Freed-block quarantine queue size in bytes (0 to disable)"
depends on COMPILER_KASAN
default 8192
range 0 65536
help
When non-zero, freed heap blocks are held in a FIFO quarantine for this
many bytes total before being returned to the allocator. Quarantined
blocks remain poisoned, allowing KASAN to catch use-after-free accesses
for some time after the block is freed. Increases peak memory usage by
the configured amount. Set to 0 to disable (frees memory immediately).
config KASAN_NO_HALT
bool "Continue execution after KASAN error (no abort)"
depends on COMPILER_KASAN
default n
help
When enabled, KASAN prints the error report but does not call
esp_system_abort(). Execution continues after each violation.
This is useful for test applications that need to verify multiple
KASAN detections in a single boot cycle.
Not recommended for production or debugging real bugs, as continued
execution after a memory safety violation may cause undefined behaviour.
endmenu # Kernel Address Sanitizer (KASAN)
endmenu # Compiler Options endmenu # Compiler Options
menu "Component config" menu "Component config"
@@ -890,3 +967,4 @@ mainmenu "Espressif IoT Development Framework Configuration"
- CONFIG_ESP_WIFI_NAN_SECURITY - CONFIG_ESP_WIFI_NAN_SECURITY
- CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS - CONFIG_USB_HOST_EXT_PORT_RESET_ATTEMPTS
- CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION - CONFIG_GDMA_ENABLE_WEIGHTED_ARBITRATION
- CONFIG_COMPILER_KASAN
+45 -5
View File
@@ -56,6 +56,10 @@ else()
"system_time.c" "system_time.c"
"stack_check.c" "stack_check.c"
"ubsan.c") "ubsan.c")
if(CONFIG_COMPILER_KASAN)
list(APPEND srcs "kasan.c")
endif()
if(CONFIG_SOC_WDT_SUPPORTED) if(CONFIG_SOC_WDT_SUPPORTED)
list(APPEND srcs "int_wdt.c") list(APPEND srcs "int_wdt.c")
endif() endif()
@@ -110,11 +114,30 @@ else()
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u start_app_other_cores") target_link_libraries(${COMPONENT_LIB} INTERFACE "-u start_app_other_cores")
endif() endif()
# Disable stack protection in files which are involved in initialization of that feature if(CONFIG_COMPILER_KASAN)
set_source_files_properties( # Files involved in stack-protector initialisation: disable stack protector.
"startup.c" "stack_check.c" "port/cpu_start.c" # Also exclude from KASAN: cpu_start.c runs before kasan_init_shadow() and
PROPERTIES COMPILE_FLAGS # panic.c / startup.c must not recurse into KASAN during crash handling.
-fno-stack-protector) 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") target_linker_script(${COMPONENT_LIB} INTERFACE "ld/${target}/memory.ld.in")
@@ -137,6 +160,23 @@ endif()
# due to -ffunction-sections -Wl,--gc-sections options. # due to -ffunction-sections -Wl,--gc-sections options.
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u __ubsan_include") 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") target_link_libraries(${COMPONENT_LIB} INTERFACE "-u esp_system_include_startup_funcs")
# [refactor-todo] requirements due to init code, should be removable # [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) FORCE_INLINE_ATTR IRAM_ATTR void init_cpu(void)
{ {
#ifdef __riscv #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()) { if (esp_cpu_dbgr_is_attached()) {
/* Let debugger some time to detect that target started, halt it, enable ebreaks and resume. /* Let debugger some time to detect that target started, halt it, enable ebreaks and resume.
500ms should be enough. */ 500ms should be enough. */
@@ -408,15 +420,6 @@ FORCE_INLINE_ATTR IRAM_ATTR void init_cpu(void)
esp_rom_delay_us(100000); 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 #endif
/* NOTE: When ESP-TEE is enabled, this sets up the callback function /* 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_internal.h"
#include "esp_private/panic_reason.h" #include "esp_private/panic_reason.h"
#if CONFIG_COMPILER_KASAN
#include "esp_kasan.h"
#endif
#if SOC_WDT_SUPPORTED || SOC_RTC_WDT_SUPPORTED #if SOC_WDT_SUPPORTED || SOC_RTC_WDT_SUPPORTED
#include "hal/wdt_types.h" #include "hal/wdt_types.h"
#include "hal/wdt_hal.h" #include "hal/wdt_hal.h"
@@ -128,6 +132,13 @@ void busy_wait(void)
static void panic_handler(void *frame, bool pseudo_excause) 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 /* 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. * 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: 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) 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 # 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) 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()
endif() endif()
set(ldfragments linker.lf)
idf_component_register(SRCS "${srcs}" idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS ${includes} INCLUDE_DIRS ${includes}
PRIV_INCLUDE_DIRS ${priv_includes} PRIV_INCLUDE_DIRS ${priv_includes}
LDFRAGMENTS linker.lf LDFRAGMENTS ${ldfragments}
PRIV_REQUIRES soc) PRIV_REQUIRES soc esp_system)
if(CONFIG_HEAP_TLSF_USE_ROM_IMPL AND NOT BOOTLOADER_BUILD) if(CONFIG_HEAP_TLSF_USE_ROM_IMPL AND NOT BOOTLOADER_BUILD)
# After registering the component, set the tlsf_set_rom_patches symbol as undefined # After registering the component, set the tlsf_set_rom_patches symbol as undefined
@@ -110,3 +112,38 @@ else()
endif() endif()
endif() 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 "multi_heap.h"
#include "esp_log.h" #include "esp_log.h"
#include "heap_private.h" #include "heap_private.h"
#include "heap_kasan_layout.h"
#include "esp_system.h" #include "esp_system.h"
/* /*
@@ -480,13 +481,22 @@ void heap_caps_dump_all(void)
size_t heap_caps_get_allocated_size( void *ptr ) 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 // add the block owner bytes back to ptr before handing over
// to multi heap layer. // to multi heap layer.
ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(ptr); ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(ptr);
heap_t *heap = find_containing_heap(ptr); heap_t *heap = find_containing_heap(ptr);
assert(heap); assert(heap);
size_t size = multi_heap_get_allocated_size(heap->heap, ptr); 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) 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 * SPDX-License-Identifier: Apache-2.0
*/ */
@@ -12,6 +12,7 @@
#include "multi_heap.h" #include "multi_heap.h"
#include "esp_log.h" #include "esp_log.h"
#include "heap_private.h" #include "heap_private.h"
#include "heap_kasan_layout.h"
#if CONFIG_HEAP_TASK_TRACKING #if CONFIG_HEAP_TASK_TRACKING
#include "esp_heap_task_info.h" #include "esp_heap_task_info.h"
#include "esp_heap_task_info_internal.h" #include "esp_heap_task_info_internal.h"
@@ -28,9 +29,28 @@
#define CALL_HOOK(hook, ...) {} #define CALL_HOOK(hook, ...) {}
#endif #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. //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); 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 //Default alignment the multiheap allocator / tlsf will align 'unaligned' memory to, in bytes
#define UNALIGNED_MEM_ALIGNMENT_BYTES 4 #define UNALIGNED_MEM_ALIGNMENT_BYTES 4
@@ -67,6 +87,17 @@ HEAP_IRAM_ATTR void heap_caps_free( void *ptr)
return; 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)) || if ((!esp_dram_match_iram() && esp_ptr_in_diram_iram(ptr)) ||
(!esp_rtc_dram_match_rtc_iram() && esp_ptr_in_rtc_iram_fast(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 //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; uint32_t *dramAddrPtr = (uint32_t *)ptr;
ptr = (void *)dramAddrPtr[-1]; 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); heap_t *heap = find_containing_heap(block_owner_ptr);
assert(heap != NULL && "free() target pointer is outside heap areas"); assert(heap != NULL && "free() target pointer is outside heap areas");
#if CONFIG_HEAP_TASK_TRACKING #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 #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); multi_heap_free(heap->heap, block_owner_ptr);
CALL_HOOK(esp_heap_trace_free_hook, 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) { 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 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++) { for (int prio = 0; prio < SOC_MEMORY_TYPE_NO_PRIOS; prio++) {
//Iterate over heaps and check capabilities at this priority //Iterate over heaps and check capabilities at this priority
heap_t *heap; 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, //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 //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. //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 alignment, MULTI_HEAP_BLOCK_OWNER_SIZE()); // int overflow checked above
if (ret != NULL) { if (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING #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); MULTI_HEAP_SET_BLOCK_OWNER(ret);
ret = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(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 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); CALL_HOOK(esp_heap_trace_alloc_hook, iptr, size, caps);
return iptr; return iptr;
} }
} else { } else {
//Just try to alloc, nothing special. //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()); alignment, MULTI_HEAP_BLOCK_OWNER_SIZE());
if (ret != NULL) { if (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING #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); MULTI_HEAP_SET_BLOCK_OWNER(ret);
ret = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(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); CALL_HOOK(esp_heap_trace_alloc_hook, ret, size, caps);
return ret; return ret;
} }
@@ -236,31 +283,43 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_realloc_base( void *ptr, size_t siz
return NULL; return NULL;
} }
//The pointer to memory may be aliased, we need to /*
//recover the corresponding address before to manage a new allocation: * DIRAM aliasing detection must happen on the original user pointer:
if(esp_ptr_in_diram_iram((void *)ptr)) { * dram_alloc_to_iram_addr stores the underlying DRAM address one word
uint32_t *dram_addr = (uint32_t *)ptr; * before the IRAM pointer, so the KASAN redzone shift (which moves the
dram_ptr = (void *)dram_addr[-1]; * 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); dram_ptr = MULTI_HEAP_REMOVE_BLOCK_OWNER_OFFSET(dram_ptr);
heap = find_containing_heap(dram_ptr); heap = find_containing_heap(dram_ptr);
assert(heap != NULL && "realloc() pointer is outside heap areas"); assert(heap != NULL && "realloc() pointer is outside heap areas");
//with pointers that reside on diram space, we avoid using /* with pointers that reside on diram space, we avoid using
//the realloc implementation due to address translation issues, * the realloc implementation due to address translation issues,
//instead force a malloc/copy/free * instead force a malloc/copy/free */
ptr_in_diram_case = true; ptr_in_diram_case = true;
raw_ptr = NULL; /* not used in the diram path */
} else { } 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"); assert(heap != NULL && "realloc() pointer is outside heap areas");
} }
// shift ptr by block owner offset. Since the ptr returned to the user const size_t alloc_size = KASAN_ADD_RZ(size);
// 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);
// are the existing heap's capabilities compatible with the // are the existing heap's capabilities compatible with the
// requested ones? // 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) // (which will resize the block if it can)
#if CONFIG_HEAP_TASK_TRACKING #if CONFIG_HEAP_TASK_TRACKING
size_t old_size = multi_heap_get_full_block_size(heap->heap, ptr); size_t old_size = multi_heap_get_full_block_size(heap->heap, raw_ptr);
TaskHandle_t old_task = MULTI_HEAP_GET_BLOCK_OWNER(ptr); TaskHandle_t old_task = MULTI_HEAP_GET_BLOCK_OWNER(raw_ptr);
#endif #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) { if (r != NULL) {
MULTI_HEAP_SET_BLOCK_OWNER(r); MULTI_HEAP_SET_BLOCK_OWNER(r);
#if CONFIG_HEAP_TASK_TRACKING #if CONFIG_HEAP_TASK_TRACKING
heap_caps_update_per_task_info_realloc(heap, 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), MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(r),
old_size, old_task, old_size, old_task,
multi_heap_get_full_block_size(heap->heap, r), 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 #endif
r = MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(r); 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); CALL_HOOK(esp_heap_trace_alloc_hook, r, size, caps);
return r; 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) { if(ptr_in_diram_case) {
old_size = multi_heap_get_allocated_size(heap->heap, dram_ptr); old_size = multi_heap_get_allocated_size(heap->heap, dram_ptr);
} else { } 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); assert(old_size > 0);
// do not copy the block owner bytes old_size = MULTI_HEAP_REMOVE_BLOCK_OWNER_SIZE(old_size);
memcpy(new_p, MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ptr), MIN(size, old_size)); /* Subtract KASAN redzone overhead to get the actual user-data size. */
// add the block owner bytes to ptr since they are removed in heap_caps_free if (old_size > 2 * KASAN_RZ) {
heap_caps_free(MULTI_HEAP_ADD_BLOCK_OWNER_OFFSET(ptr)); old_size -= 2 * KASAN_RZ;
}
memcpy(new_p, ptr, MIN(size, old_size));
heap_caps_free(ptr);
return new_p; 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
+27 -1
View File
@@ -6,7 +6,7 @@ Heap Memory Debugging
Overview Overview
-------- --------
ESP-IDF integrates tools for requesting :ref:`heap information <heap-information>`, :ref:`heap corruption detection <heap-corruption>`, and :ref:`heap tracing <heap-tracing>`. These can help track down memory-related bugs. ESP-IDF integrates tools for requesting :ref:`heap information <heap-information>`, :ref:`heap corruption detection <heap-corruption>`, :ref:`kernel address sanitizer (KASAN) <heap-kasan>`, and :ref:`heap tracing <heap-tracing>`. These can help track down memory-related bugs.
For general information about the heap memory allocator, see :doc:`Heap Memory Allocation </api-reference/system/mem_alloc>`. For general information about the heap memory allocator, see :doc:`Heap Memory Allocation </api-reference/system/mem_alloc>`.
@@ -195,6 +195,32 @@ Calls to :cpp:func:`heap_caps_check_integrity` or :cpp:func:`heap_caps_check_int
- For allocated heap blocks, the behavior is the same as for the Light Impact mode. The canary bytes ``0xABBA1234`` and ``0xBAAD5678`` are checked at the head and tail of each allocated buffer, and any variation indicates a buffer overrun or underrun. - For allocated heap blocks, the behavior is the same as for the Light Impact mode. The canary bytes ``0xABBA1234`` and ``0xBAAD5678`` are checked at the head and tail of each allocated buffer, and any variation indicates a buffer overrun or underrun.
.. _heap-kasan:
Kernel Address Sanitizer (KASAN)
--------------------------------
KASAN is a compiler-assisted heap and DRAM memory safety checker. When enabled, GCC instruments memory loads and stores with runtime checks against a shadow memory region. Violations (buffer overflows, underflows, use-after-free, etc.) are reported at the point of access.
Enable it under ``Component config`` > ``Compiler options`` > ``Enable Kernel Address Sanitizer (KASAN)`` (see :ref:`CONFIG_COMPILER_KASAN`). The option is currently marked experimental: turn on ``Make experimental features visible`` (see :ref:`CONFIG_IDF_EXPERIMENTAL_FEATURES`) first.
KASAN is most useful during development and debugging:
- Detects out-of-bounds heap accesses via configurable allocation redzones (see :ref:`CONFIG_KASAN_HEAP_REDZONE_SIZE`)
- Can catch use-after-free when the freed-block quarantine is enabled (see :ref:`CONFIG_KASAN_QUARANTINE_SIZE`)
- Instruments most application and component code; low-level HAL/ROM/bootloader code is excluded automatically
Trade-offs to keep in mind:
- Code size for instrumented components typically grows by 1.5–3x
- Shadow memory reserves roughly 42–64 KiB of internal DRAM (target-dependent)
- Runtime overhead is significant; do not enable in production firmware
KASAN uses its own heap hooks and redzone scheme. Do not enable heap poisoning at the same time — leave :ref:`CONFIG_HEAP_CORRUPTION_DETECTION` at ``Basic (no poisoning)`` (the default).
For deliberate fault injection and regression testing, see the ``kasan_test`` application under ``tools/test_apps/system/kasan_test``.
.. _heap-task-tracking: .. _heap-task-tracking:
Heap Task Tracking Heap Task Tracking
+27 -1
View File
@@ -6,7 +6,7 @@
概述 概述
-------- --------
ESP-IDF 集成了用于请求 :ref:`堆内存信息 <heap-information>`、:ref:`堆内存损坏检测 <heap-corruption>` 和 :ref:`堆内存跟踪 <heap-tracing>` 的工具,有助于跟踪内存相关错误。 ESP-IDF 集成了用于请求 :ref:`堆内存信息 <heap-information>`、:ref:`堆内存损坏检测 <heap-corruption>`、:ref:`内核地址消毒器 (KASAN) <heap-kasan>` 和 :ref:`堆内存跟踪 <heap-tracing>` 的工具,有助于跟踪内存相关错误。
有关堆内存分配器的基本信息,请参阅 :doc:`堆内存分配 </api-reference/system/mem_alloc>`。 有关堆内存分配器的基本信息,请参阅 :doc:`堆内存分配 </api-reference/system/mem_alloc>`。
@@ -195,6 +195,32 @@ ESP-IDF 集成了用于请求 :ref:`堆内存信息 <heap-information>`、:ref:`
- 对于已分配的堆内存块,检测器的检查模式与轻量级模式相同,即在每个分配的缓冲区头部和尾部检查 canary 字节 ``0xABBA1234`` 和 ``0xBAAD5678``,检测到任何其他字节都表示缓冲区越界或下溢。 - 对于已分配的堆内存块,检测器的检查模式与轻量级模式相同,即在每个分配的缓冲区头部和尾部检查 canary 字节 ``0xABBA1234`` 和 ``0xBAAD5678``,检测到任何其他字节都表示缓冲区越界或下溢。
.. _heap-kasan:
内核地址消毒器 (KASAN)
----------------------
KASAN 是一种由编译器辅助的堆内存和 DRAM 内存安全检查工具。启用后,GCC 会为内存加载和存储操作插入运行时检查,对照影子内存区域进行校验。一旦发生违规访问(缓冲区溢出、下溢、释放后使用等),会在访问发生处立即报告。
可在 ``Component config`` > ``Compiler options`` > ``Enable Kernel Address Sanitizer (KASAN)`` 中启用该功能(参见 :ref:`CONFIG_COMPILER_KASAN`)。该选项目前标记为实验性功能,需先开启 ``Make experimental features visible``\ (参见 :ref:`CONFIG_IDF_EXPERIMENTAL_FEATURES`)。
KASAN 在开发和调试阶段最为有用:
- 通过可配置的分配红区检测堆内存越界访问(参见 :ref:`CONFIG_KASAN_HEAP_REDZONE_SIZE`)
- 启用已释放内存块隔离队列后,可捕获释放后使用问题(参见 :ref:`CONFIG_KASAN_QUARANTINE_SIZE`)
- 会为大多数应用程序和组件代码插桩;底层 HAL/ROM/bootloader 代码会被自动排除
需要权衡的方面:
- 插桩组件的代码大小通常会增加 1.5–3 倍
- 影子内存会占用约 42–64 KiB 的内部 DRAM(取决于目标芯片)
- 运行时开销较大,请勿在量产固件中启用
KASAN 使用自己的堆内存钩子和红区方案。请勿同时启用堆内存毒化功能,应将 :ref:`CONFIG_HEAP_CORRUPTION_DETECTION` 保持为 ``Basic (no poisoning)``\ (默认值)。
如需进行人为故障注入和回归测试,请参阅 ``tools/test_apps/system/kasan_test`` 下的 ``kasan_test`` 应用程序。
.. _heap-task-tracking: .. _heap-task-tracking:
堆任务跟踪 堆任务跟踪
@@ -105,6 +105,12 @@ tools/test_apps/system/init_array:
depends_filepatterns: depends_filepatterns:
- tools/tools.json - tools/tools.json
tools/test_apps/system/kasan_test:
depends_components:
- *common_components
- esp_system
- heap
tools/test_apps/system/log: tools/test_apps/system/log:
disable_test: disable_test:
- if: IDF_TARGET not in ["esp32", "esp32c3"] - if: IDF_TARGET not in ["esp32", "esp32c3"]
@@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(kasan_test)
@@ -0,0 +1,72 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | --------- |
# KASAN Test Application
This test application validates the Kernel Address Sanitizer (KASAN) integration
in ESP-IDF by deliberately triggering memory safety bugs and checking that KASAN
detects and reports them before calling the panic handler.
## Test Cases
| Test | Description | Expected Outcome |
|------|-------------|-----------------|
| `overflow` | 1-byte write past end of 16-byte heap allocation | KASAN WRITE error + panic |
| `use_after_free` | Write to freed heap block | KASAN WRITE error + panic |
| `uaf_read` | Read from freed heap block | KASAN READ error + panic |
| `underflow` | Write before start of allocation (into left redzone) | KASAN WRITE error + panic |
| `large_overflow` | `memset` of 16 bytes into an 8-byte buffer | KASAN WRITE error + panic |
| `no_bug` | Clean alloc/use/free cycle | Completes without panic |
| `asan stubs valid access no error` | Direct calls to every sized `__asan_load<N>_noabort` and `__asan_store<N>_noabort` stub (N in {1, 2, 4, 8, 16, N}) on a valid buffer | Completes without panic; covers all 12 stubs |
| `asan stubs poisoned access all sizes` (no_halt only) | Same 12 stubs called on a freed pointer | Exactly 12 KASAN errors reported |
## Building
```bash
cd tools/test_apps/system/kasan_test
idf.py set-target esp32c6
idf.py build
```
Or to select a specific test case:
```bash
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.ci.overflow" build
```
## Prerequisites
The following Kconfig options must be set (they are pre-configured in
`sdkconfig.defaults`):
- `CONFIG_IDF_EXPERIMENTAL_FEATURES=y` – Required to expose KASAN in menuconfig
- `CONFIG_COMPILER_KASAN=y` – Enable KASAN instrumentation
- `CONFIG_ESP_TASK_WDT_EN=n` – Unity menu blocks IDLE until you press Enter or
select a test; required for manual `idf.py monitor` as well as pytest
`CONFIG_COMPILER_KASAN` automatically selects `CONFIG_HEAP_USE_HOOKS`. Redzone
size (8 bytes), quarantine size (8192 bytes), and heap poisoning (disabled) use
their Kconfig defaults — no extra overrides are needed.
The `sdkconfig.ci.*` files add only mode-specific options (e.g. `CONFIG_KASAN_NO_HALT`
for the all-in-one run, `CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT` for halt-mode pytest).
## Running pytest
```bash
pytest pytest_kasan.py --target esp32c6 -v
```
## Memory and Performance Impact
| Target | Shadow Memory | Code Size Overhead | Free Heap Impact |
|--------|--------------|-------------------|-----------------|
| ESP32 | ~42 KiB (internal SRAM) | ~1.5-3x instrumented components | ~14% of free heap |
| ESP32-S3 | ~60 KiB | ~1.5-3x | ~16% of free heap |
| ESP32-C3 | ~54 KiB | ~1.5-3x | ~16% of free heap |
| ESP32-C6 | ~64 KiB | ~1.5-3x | ~14% of free heap |
The shadow array is placed in DRAM via the `DRAM_ATTR` attribute on
`kasan_shadow_mem` (mapped into `dram0_data` by the standard `.dram1` linker
mapping). On targets with PSRAM the shadow currently stays in internal SRAM;
placing it in external RAM is not yet supported.
@@ -0,0 +1,3 @@
idf_component_register(SRCS "kasan_test_main.c"
INCLUDE_DIRS "."
PRIV_REQUIRES esp_system heap unity)
@@ -0,0 +1,250 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* Unity-based KASAN test application.
*
* With CONFIG_KASAN_NO_HALT: all tests run in one boot cycle; each test
* triggers a bug and asserts that kasan_get_error_count() increased.
*
* Without CONFIG_KASAN_NO_HALT (default): select individual tests via the
* Unity menu. Each error test causes an abort; pytest verifies the panic.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "sdkconfig.h"
#include "unity.h"
#include "esp_log.h"
#include "esp_kasan.h"
static const char *TAG = "kasan_test";
/* ---------------------------------------------------------------------- */
TEST_CASE("heap buffer overflow", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
char *buf = (char *)malloc(16);
TEST_ASSERT_NOT_NULL(buf);
memset(buf, 'A', 16);
buf[16] = 'X'; /* write into right redzone */
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_GREATER_THAN(0, kasan_get_error_count());
#endif
free(buf);
}
/* ---------------------------------------------------------------------- */
TEST_CASE("use-after-free write", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
char *buf = (char *)malloc(32);
TEST_ASSERT_NOT_NULL(buf);
memset(buf, 0, 32);
free(buf);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuse-after-free"
buf[4] = 'Y';
#pragma GCC diagnostic pop
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_GREATER_THAN(0, kasan_get_error_count());
#endif
}
/* ---------------------------------------------------------------------- */
TEST_CASE("use-after-free read", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
int *buf = (int *)malloc(4 * sizeof(int));
TEST_ASSERT_NOT_NULL(buf);
buf[0] = 42;
free(buf);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuse-after-free"
volatile int val = buf[0];
(void)val;
#pragma GCC diagnostic pop
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_GREATER_THAN(0, kasan_get_error_count());
#endif
}
/* ---------------------------------------------------------------------- */
TEST_CASE("heap buffer underflow", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
char *buf = (char *)malloc(16);
TEST_ASSERT_NOT_NULL(buf);
buf[-1] = 'Z'; /* write into left redzone */
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_GREATER_THAN(0, kasan_get_error_count());
/* The underflow write corrupted the redzone header; skip free to avoid
* side-effects from a bad header read. Small intentional leak. */
#else
free(buf);
#endif
}
/* ---------------------------------------------------------------------- */
TEST_CASE("large heap overflow", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
char *buf = (char *)malloc(8);
TEST_ASSERT_NOT_NULL(buf);
for (int i = 0; i < 8; i++) {
buf[i] = 0;
}
buf[8] = 'X'; /* overflow into right redzone */
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_GREATER_THAN(0, kasan_get_error_count());
#endif
free(buf);
}
/* ---------------------------------------------------------------------- */
TEST_CASE("no false positive", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
char *buf = (char *)malloc(32);
TEST_ASSERT_NOT_NULL(buf);
memset(buf, 'A', 32);
volatile char c = buf[31];
(void)c;
free(buf);
#if CONFIG_KASAN_NO_HALT
TEST_ASSERT_EQUAL_UINT32(0, kasan_get_error_count());
#endif
ESP_LOGI(TAG, "no-bug test PASSED");
}
/* ---------------------------------------------------------------------- */
/*
* GCC -fsanitize=kernel-address instrumentation calls a sized family of
* stubs: __asan_load<N>_noabort and __asan_store<N>_noabort for
* N in {1, 2, 4, 8, 16, N}. The two tests below exercise every stub
* directly so the runtime contract (link symbol present, valid access
* passes, poisoned access reports an error) is verified for each size.
*
* Calling the stubs directly is intentional: GCC's choice of which sized
* stub to emit depends on access size, alignment, and target ISA, so
* relying on instrumentation alone leaves gaps (this is exactly how the
* 16-byte variants were missed previously: the dedicated test code
* never tripped GCC into emitting `__asan_*16_noabort`).
*/
extern void __asan_load1_noabort(void *addr);
extern void __asan_load2_noabort(void *addr);
extern void __asan_load4_noabort(void *addr);
extern void __asan_load8_noabort(void *addr);
extern void __asan_load16_noabort(void *addr);
/*
* The size parameter for the variable-size ASAN check stubs is declared as
* `int` by GCC's builtin, so we have to match that type here even though
* the size in practice is non-negative. Using `size_t` would trigger
* -Werror=builtin-declaration-mismatch under newer GCC (15+).
*/
extern void __asan_loadN_noabort(void *addr, int size);
extern void __asan_store1_noabort(void *addr);
extern void __asan_store2_noabort(void *addr);
extern void __asan_store4_noabort(void *addr);
extern void __asan_store8_noabort(void *addr);
extern void __asan_store16_noabort(void *addr);
extern void __asan_storeN_noabort(void *addr, int size);
/* All 12 sized ASAN check stubs in one call set.
* Returns the number of stub calls made (always 12). */
static unsigned exercise_all_asan_stubs(void *p)
{
__asan_load1_noabort(p);
__asan_load2_noabort(p);
__asan_load4_noabort(p);
__asan_load8_noabort(p);
__asan_load16_noabort(p);
__asan_loadN_noabort(p, 7);
__asan_store1_noabort(p);
__asan_store2_noabort(p);
__asan_store4_noabort(p);
__asan_store8_noabort(p);
__asan_store16_noabort(p);
__asan_storeN_noabort(p, 7);
return 12;
}
TEST_CASE("asan stubs valid access no error", "[kasan]")
{
#if CONFIG_KASAN_NO_HALT
kasan_reset_error_count();
#endif
/* malloc(64) gives us 64 valid bytes; the largest stub reads 16 bytes
* starting at p, so p..p+63 is safely inside the allocation. */
char *buf = (char *)malloc(64);
TEST_ASSERT_NOT_NULL(buf);
memset(buf, 'A', 64);
unsigned calls = exercise_all_asan_stubs(buf);
TEST_ASSERT_EQUAL_UINT(12, calls);
#if CONFIG_KASAN_NO_HALT
/* Each stub touched only valid bytes; no error should have been raised. */
TEST_ASSERT_EQUAL_UINT32(0, kasan_get_error_count());
#endif
free(buf);
ESP_LOGI(TAG, "asan stubs valid-access test PASSED");
}
#if CONFIG_KASAN_NO_HALT
TEST_CASE("asan stubs poisoned access all sizes", "[kasan]")
{
kasan_reset_error_count();
/* A freed pointer sits in the quarantine FIFO with its full block
* shadow poisoned, so every stub size will trip the shadow check. */
char *buf = (char *)malloc(64);
TEST_ASSERT_NOT_NULL(buf);
memset(buf, 0, 64);
free(buf);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuse-after-free"
unsigned calls = exercise_all_asan_stubs(buf);
#pragma GCC diagnostic pop
TEST_ASSERT_EQUAL_UINT(12, calls);
/* Exactly one error per stub call: 6 sized loads + 6 sized stores. */
TEST_ASSERT_EQUAL_UINT32(12, kasan_get_error_count());
ESP_LOGI(TAG, "asan stubs poisoned-access test PASSED (12 errors)");
}
#endif /* CONFIG_KASAN_NO_HALT */
/* ---------------------------------------------------------------------- */
void app_main(void)
{
ESP_LOGI(TAG, "KASAN test application starting");
unity_run_menu();
}
@@ -0,0 +1,104 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""
Pytest test cases for the KASAN Unity test application.
Two configurations:
- no_halt: all tests run in one boot cycle (CONFIG_KASAN_NO_HALT=y).
The Unity runner executes every test; each verifies the
KASAN error count.
- halt: each error test triggers an abort. pytest selects one test
at a time via the Unity menu, expecting a panic.
"""
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# ---------------------------------------------------------------------------
# no_halt configuration: all tests pass in one run
# ---------------------------------------------------------------------------
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['no_halt'], indirect=True)
def test_kasan_no_halt_all(dut: Dut) -> None:
"""Run all KASAN tests in one boot cycle with CONFIG_KASAN_NO_HALT=y."""
dut.expect('KASAN test application starting', timeout=15)
# Send '*' to Unity menu to run all tests
dut.write('*')
dut.expect(r'\d+ Tests \d+ Failures \d+ Ignored', timeout=45)
# ---------------------------------------------------------------------------
# halt configuration: each error test causes an abort
# ---------------------------------------------------------------------------
def _run_halt_test(dut: Dut, test_name: str) -> None:
"""Select a test from Unity menu and expect KASAN abort."""
dut.expect('KASAN test application starting', timeout=15)
dut.write('"' + test_name + '"')
dut.expect(r'KASAN error: (WRITE|READ) of size \d+ at 0x', timeout=20)
@pytest.mark.generic
@pytest.mark.timeout(120)
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_overflow(dut: Dut) -> None:
_run_halt_test(dut, 'heap buffer overflow')
@pytest.mark.generic
@pytest.mark.timeout(120)
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_uaf_write(dut: Dut) -> None:
_run_halt_test(dut, 'use-after-free write')
@pytest.mark.generic
@pytest.mark.timeout(120)
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_uaf_read(dut: Dut) -> None:
_run_halt_test(dut, 'use-after-free read')
@pytest.mark.generic
@pytest.mark.timeout(120)
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_underflow(dut: Dut) -> None:
_run_halt_test(dut, 'heap buffer underflow')
@pytest.mark.generic
@pytest.mark.timeout(120)
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_large_overflow(dut: Dut) -> None:
_run_halt_test(dut, 'large heap overflow')
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_no_false_positive(dut: Dut) -> None:
"""No-bug test should complete without KASAN error."""
dut.expect('KASAN test application starting', timeout=15)
dut.write('"no false positive"')
dut.expect('no-bug test PASSED', timeout=15)
@pytest.mark.generic
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True)
def test_kasan_halt_asan_stubs_valid_access(dut: Dut) -> None:
"""Direct calls to every sized __asan_*_noabort stub on a valid buffer
must link and run without raising an error."""
dut.expect('KASAN test application starting', timeout=15)
dut.write('"asan stubs valid access no error"')
dut.expect('asan stubs valid-access test PASSED', timeout=15)
@@ -0,0 +1,2 @@
# Halt on panic so pytest can read the register dump (not KASAN-specific).
CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y
@@ -0,0 +1,2 @@
# Run all tests in one boot cycle without aborting on errors
CONFIG_KASAN_NO_HALT=y
@@ -0,0 +1,6 @@
# Minimal KASAN enablement — everything else stays at Kconfig defaults
# (redzone=8, quarantine=8192, heap poisoning=disabled, HEAP_USE_HOOKS selected).
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_COMPILER_KASAN=y
CONFIG_ESP_TASK_WDT_EN=n