feat(build): support KASAN in the build system v2 (cmakev2)

Mirror Kernel Address Sanitizer support into cmakev2 so that
CONFIG_COMPILER_KASAN instruments application code the same way build system v1
does.
This commit is contained in:
Sudeep Mohanty
2026-07-31 11:01:02 +02:00
parent cc799dbcc7
commit 9c778c954c
6 changed files with 166 additions and 31 deletions

View File

@@ -430,39 +430,12 @@ 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.
# The exclusion set itself is defined in tools/cmake/kasan.cmake and shared with
# the Build system v2, which applies it in idf_build_library().
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.
include("${CMAKE_CURRENT_LIST_DIR}/tools/cmake/kasan.cmake")
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
)
kasan_filter_excluded_components(__kasan_excluded_components ${__kasan_all_components})
foreach(__kasan_comp ${__kasan_excluded_components})
set(__kasan_lib "__idf_${__kasan_comp}")

View File

@@ -96,6 +96,10 @@ idf_build_set_property(SET_COMPILER_OPTIMIZATION NO)
# file name, which LTO does not preserve, so it must never be compiled or linked
# with LTO regardless of the application's CONFIG_COMPILER_LTO_* options.
idf_build_set_property(SET_COMPILER_LTO NO)
# The bootloader runs before kasan_init_shadow() and carries no sanitizer
# runtime, so it must never be instrumented regardless of the application's
# CONFIG_COMPILER_KASAN option.
idf_build_set_property(SET_COMPILER_KASAN NO)
# Perform internal IDF project initialisation
idf_project_init()

61
tools/cmake/kasan.cmake Normal file
View File

@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
# Kernel Address Sanitizer (CONFIG_COMPILER_KASAN) exclusion policy.
#
# Included by both build systems so that the set of components excluded from
# instrumentation has a single definition. The only build system API relied on
# here is idf_component_get_property(), which both provide with the same
# signature. Applying the result is left to each build system, because the
# component-name to library-target mapping differs between them.
# Store in <var> the subset of the given component names that must not be
# compiled with KASAN instrumentation. A component qualifies either because it
# is part of the built-in low-level set below, or because it declares the
# NO_KASAN component property in its own CMakeLists.txt.
#
# 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.
function(kasan_filter_excluded_components var)
set(excluded_components
hal soc esp_rom
spi_flash
esp_hw_support
bootloader_support
freertos
heap)
# 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.
set(excluded_pattern "^esp_hal_")
set(excluded "")
foreach(component_name IN LISTS ARGN)
if(component_name IN_LIST excluded_components
OR component_name MATCHES "${excluded_pattern}")
list(APPEND excluded "${component_name}")
continue()
endif()
# Any other component, including one shipped by a user or pulled from
# the component registry, opts out by setting the NO_KASAN component
# property in its own CMakeLists.txt.
idf_component_get_property(no_kasan "${component_name}" NO_KASAN)
if(no_kasan)
list(APPEND excluded "${component_name}")
endif()
endforeach()
set(${var} "${excluded}" PARENT_SCOPE)
endfunction()

View File

@@ -7,6 +7,8 @@ include(utilities)
include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)
include(component_validation)
# Shared with the Build system v1: single definition of the KASAN exclusion set.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/kasan.cmake)
#[[api
.. cmakev2:function:: idf_build_set_property
@@ -317,6 +319,57 @@ function(__idf_build_link_whole_archive target scope library)
target_link_libraries(${target} ${scope} ${library})
endfunction()
#[[
__kasan_exclude_components(<library>)
Compile the low-level components linked to ``library`` without Kernel
Address Sanitizer instrumentation.
The exclusion set is defined once in ``tools/cmake/kasan.cmake`` and shared
with the Build system v1. Called from ``idf_build_library`` once
LIBRARY_COMPONENTS_LINKED is populated, so every component target already
exists and ``-fno-sanitize`` is appended after the global ``-fsanitize``
added while the component was processed, which is what makes it win.
A subproject that opted out of instrumentation altogether by setting the
SET_COMPILER_KASAN build property to NO never had ``-fsanitize`` applied, so
there is nothing to undo and this is a no-op there.
#]]
function(__kasan_exclude_components library)
idf_build_get_property(set_compiler_kasan SET_COMPILER_KASAN)
if(NOT DEFINED set_compiler_kasan OR set_compiler_kasan STREQUAL "")
set(set_compiler_kasan YES)
endif()
if(NOT CONFIG_COMPILER_KASAN OR NOT set_compiler_kasan)
return()
endif()
idf_library_get_property(components_linked "${library}" LIBRARY_COMPONENTS_LINKED)
kasan_filter_excluded_components(excluded ${components_linked})
foreach(component_name IN LISTS excluded)
idf_component_get_property(component_real_target "${component_name}" COMPONENT_REAL_TARGET)
idf_component_get_property(component_real_target_type "${component_name}" COMPONENT_REAL_TARGET_TYPE)
# Components that created no target, and INTERFACE libraries, have no
# sources to de-instrument. Adding an INTERFACE compile option would
# also propagate -fno-sanitize to every consumer, including the
# application under test.
if(NOT component_real_target OR "${component_real_target}" STREQUAL "NOTFOUND")
continue()
endif()
if(NOT "${component_real_target_type}" STREQUAL "STATIC_LIBRARY")
continue()
endif()
# idf_build_library may run more than once per configure. Appending the
# option again is harmless: CMake de-duplicates compile options, and
# every copy lands after the global -fsanitize added while the component
# was processed, so the surviving one still wins.
target_compile_options("${component_real_target}" PRIVATE "-fno-sanitize=kernel-address")
endforeach()
endfunction()
#[[api
.. cmakev2:function:: idf_build_library
@@ -433,6 +486,13 @@ function(idf_build_library library)
idf_library_set_property("${library}" LIBRARY_COMPONENT_INTERFACES_LINKED "${component_interface}" APPEND)
endforeach()
# Kernel Address Sanitizer (CONFIG_COMPILER_KASAN): de-instrument the
# low-level components. Applied here, once the set of components linked to
# the library is known and every component target already exists, so that
# -fno-sanitize lands after the global -fsanitize added while the component
# was processed and therefore wins.
__kasan_exclude_components("${library}")
# Collect linker fragment files from all components linked to the library
# interface and store them in the __LDGEN_FRAGMENT_FILES files. This
# property is used by ldgen to generate template-based linker scripts.

View File

@@ -861,6 +861,18 @@ endfunction()
List of linker script files added to the link command (with ``-T``) for the
component.
.. cmakev2:component_property:: NO_KASAN
When set to a true value, the component is compiled without Kernel Address
Sanitizer instrumentation (``-fno-sanitize=kernel-address``) while
``CONFIG_COMPILER_KASAN`` is enabled. Set it on a component that runs before
the sanitizer shadow is initialised, executes with the flash cache disabled,
or is otherwise too low-level to instrument.
The low-level ESP-IDF components are excluded already; the built-in set is
defined in ``tools/cmake/kasan.cmake`` and shared with the CMake-based build
system v1, which honours this property as well.
#]]
#[[api

View File

@@ -356,6 +356,31 @@ function(__init_project_configuration)
list(APPEND compile_options "-fstack-protector-all")
endif()
# Kernel Address Sanitizer (CONFIG_COMPILER_KASAN): instrument every memory
# load and store with a shadow-memory check. The flag goes into the C and C++
# options rather than the language-agnostic list because the assembler does
# not accept -fsanitize. Low-level components are de-instrumented again in
# idf_build_library(), see __kasan_exclude_components().
#
# A subproject that must never be instrumented (for example the bootloader,
# which runs before the sanitizer shadow is initialised) sets the
# SET_COMPILER_KASAN build property to NO before idf_project_init(), the
# same way it uses SET_COMPILER_LTO and SET_COMPILER_OPTIMIZATION; an unset
# property means instrumentation is allowed.
idf_build_get_property(set_compiler_kasan SET_COMPILER_KASAN)
if(NOT DEFINED set_compiler_kasan OR set_compiler_kasan STREQUAL "")
set(set_compiler_kasan YES)
endif()
if(CONFIG_COMPILER_KASAN AND set_compiler_kasan)
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()
if(CONFIG_COMPILER_DUMP_RTL_FILES)
list(APPEND compile_options "-fdump-rtl-expand")
endif()