diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ece1b802ce..a4c7b187c0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}") diff --git a/components/bootloader/subproject/CMakeLists_v2.txt b/components/bootloader/subproject/CMakeLists_v2.txt index f676d9331f6..70730dec914 100644 --- a/components/bootloader/subproject/CMakeLists_v2.txt +++ b/components/bootloader/subproject/CMakeLists_v2.txt @@ -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() diff --git a/tools/cmake/kasan.cmake b/tools/cmake/kasan.cmake new file mode 100644 index 00000000000..2c8c6831420 --- /dev/null +++ b/tools/cmake/kasan.cmake @@ -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 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() diff --git a/tools/cmakev2/build.cmake b/tools/cmakev2/build.cmake index 0f408b26114..1017154a771 100644 --- a/tools/cmakev2/build.cmake +++ b/tools/cmakev2/build.cmake @@ -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() + + 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. diff --git a/tools/cmakev2/component.cmake b/tools/cmakev2/component.cmake index 0f83bd72699..3251fcf30e4 100644 --- a/tools/cmakev2/component.cmake +++ b/tools/cmakev2/component.cmake @@ -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 diff --git a/tools/cmakev2/project.cmake b/tools/cmakev2/project.cmake index c8e229e7064..8d9caeb14de 100644 --- a/tools/cmakev2/project.cmake +++ b/tools/cmakev2/project.cmake @@ -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() diff --git a/tools/test_apps/system/.build-test-rules.yml b/tools/test_apps/system/.build-test-rules.yml index 668a388ebe7..ddb9bdc316d 100644 --- a/tools/test_apps/system/.build-test-rules.yml +++ b/tools/test_apps/system/.build-test-rules.yml @@ -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 diff --git a/tools/test_apps/system/kasan_test/pytest_kasan.py b/tools/test_apps/system/kasan_test/pytest_kasan.py index 7026fa0d4fb..c39cb350d16 100644 --- a/tools/test_apps/system/kasan_test/pytest_kasan.py +++ b/tools/test_apps/system/kasan_test/pytest_kasan.py @@ -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 # ---------------------------------------------------------------------------