Merge branch 'feat/buildv2_kasan' into 'master'

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

See merge request espressif/esp-idf!51408
This commit is contained in:
Sudeep Mohanty
2026-08-04 17:34:00 +02:00
8 changed files with 170 additions and 48 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
@@ -379,8 +432,10 @@ function(idf_build_library library)
idf_build_get_property(include_directories INCLUDE_DIRECTORIES GENERATOR_EXPRESSION)
target_include_directories("${library}" INTERFACE "${include_directories}")
# Add link options.
idf_build_get_property(link_options LINK_OPTIONS)
# Add link options. Read as a generator expression so that link options a
# component appends to the LINK_OPTIONS build property while it is processed
# below are included, not only those set before this point.
idf_build_get_property(link_options LINK_OPTIONS GENERATOR_EXPRESSION)
target_link_options(${library} INTERFACE "${link_options}")
# Include the requested components and link their interface targets to the
@@ -433,6 +488,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()

View File

@@ -115,9 +115,6 @@ tools/test_apps/system/init_array:
- tools/tools.json
tools/test_apps/system/kasan_test:
disable:
- if: IDF_BUILD_V2 == "1"
reason: 'KASAN is not yet supported under build system v2. TODO: IDF-15864'
depends_components:
- *common_components
- esp_system

View File

@@ -11,22 +11,10 @@ Two configurations:
at a time via the Unity menu, expecting a panic.
"""
import os
import pytest
from pytest_embedded import Dut
from pytest_embedded_idf.utils import idf_parametrize
# KASAN instrumentation is not yet emitted under build system v2, so the test
# binary never reports a KASAN error and these cases would always fail. Skip the
# whole module at collection time so no target_test job is even generated for it.
# TODO: IDF-15864
if os.environ.get('IDF_BUILD_V2') == '1':
pytest.skip(
'KASAN is not yet supported under build system v2. TODO: IDF-15864',
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# no_halt configuration: all tests pass in one run
# ---------------------------------------------------------------------------