mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
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:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 */
|
||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
@@ -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 */
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user