diff --git a/.github/workflows/cpp-car.yml b/.github/workflows/cpp-car.yml index 933de0f..0d8f2ab 100644 --- a/.github/workflows/cpp-car.yml +++ b/.github/workflows/cpp-car.yml @@ -177,8 +177,67 @@ jobs: run: ./scripts/run_clang_tidy.sh working-directory: car + ubsan: + needs: build-docker + permissions: + contents: read + actions: read + + runs-on: ubuntu-latest + + strategy: + matrix: + include: + - name: telemetry + path: car/components/telemetry/test_apps + pytest_pattern: pytest_telemetry_qemu.py + - name: web_server + path: car/components/web_server/test_apps + pytest_pattern: pytest_web_server_qemu.py + + container: + image: ghcr.io/ltowarek/dust-mite-cpp-devcontainer + options: --user root + credentials: + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Cache ccache + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-${{ runner.os }}-idf-ubsan-${{ matrix.name }}-${{ github.run_id }} + restore-keys: | + ccache-${{ runner.os }}-idf-ubsan-${{ matrix.name }}- + ccache-${{ runner.os }}-idf-ubsan- + + - name: Build + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + SDKCONFIG_DEFAULTS: sdkconfig.defaults;sdkconfig.defaults.qemu;sdkconfig.defaults.ubsan + run: | + source $IDF_PATH/export.sh + idf.py build + working-directory: ${{ matrix.path }} + + - name: Run QEMU tests + run: | + source $IDF_PATH/export.sh + pytest ${{ matrix.pytest_pattern }} --embedded-services idf,qemu -v + working-directory: ${{ matrix.path }} + ci-status-cpp-car: - needs: [build-docker, build, clang-format, clang-tidy] + needs: [build-docker, build, clang-format, clang-tidy, ubsan] if: always() runs-on: ubuntu-latest steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ef3aff..ca77a27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -452,6 +452,29 @@ tolerates that link failure and only requires `compile_commands.json` to be gene `clang-tidy` CI job is blocking (part of `ci-status-cpp-car`); since `WarningsAsErrors` is empty, it only fails on a genuine clang-tidy tool error, not on findings. +#### UndefinedBehaviorSanitizer (UBSan) + +A QEMU test app can opt in to building its component under test with +[UndefinedBehaviorSanitizer](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/fatal-errors.html#undefined-behavior-sanitizer-ubsan-checks). +Enable it by adding a `CONFIG__TEST_UBSAN` Kconfig boolean (default `n`) to the test +app's `main/Kconfig.projbuild`, a matching `sdkconfig.defaults.ubsan` overlay that sets it to +`y`, and guarding the sanitizer flags in the test app's `CMakeLists.txt`: + +```cmake +if(CONFIG__TEST_UBSAN) + idf_component_get_property(_lib COMPONENT_LIB) + target_compile_options(${_lib} PRIVATE "-fsanitize=undefined" "-fno-sanitize=shift-base") +endif() +``` + +Scoping the flags to just the component under test (rather than the whole app) follows ESP-IDF's +documented pattern — enabling UBSan project-wide grows code/data size too much to fit on-target, +which is also why it is never enabled for the production firmware build. A dedicated `ubsan` CI +job builds and runs each opted-in test app with `sdkconfig.defaults.ubsan` layered on top of +`sdkconfig.defaults.qemu`, using its own matrix (separate from the `build` job's matrix) so the +default test apps keep building without the sanitizer. The `ubsan` CI job is blocking (part of +`ci-status-cpp-car`). + ### Car test types Test scope and execution environment are independent choices. Scope determines what is under test; environment determines where it runs. diff --git a/car/components/telemetry/test_apps/CMakeLists.txt b/car/components/telemetry/test_apps/CMakeLists.txt index 8a716d6..b25af7e 100644 --- a/car/components/telemetry/test_apps/CMakeLists.txt +++ b/car/components/telemetry/test_apps/CMakeLists.txt @@ -4,4 +4,12 @@ set(EXTRA_COMPONENT_DIRS "../.." "../../DFRobot_AXP313A/DFRobot_AXP313A/esp_idf" set(COMPONENTS main) include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(telemetry_test) \ No newline at end of file +project(telemetry_test) + +# Opt-in via CONFIG_TELEMETRY_TEST_UBSAN (see CONTRIBUTING.md); scoped to the +# component under test since enabling UBSan project-wide grows code/data size +# too much to fit on-target. +if(CONFIG_TELEMETRY_TEST_UBSAN) + idf_component_get_property(telemetry_lib telemetry COMPONENT_LIB) + target_compile_options(${telemetry_lib} PRIVATE "-fsanitize=undefined" "-fno-sanitize=shift-base") +endif() \ No newline at end of file diff --git a/car/components/telemetry/test_apps/main/Kconfig.projbuild b/car/components/telemetry/test_apps/main/Kconfig.projbuild index 0f4ef8d..5bfc5c6 100644 --- a/car/components/telemetry/test_apps/main/Kconfig.projbuild +++ b/car/components/telemetry/test_apps/main/Kconfig.projbuild @@ -8,4 +8,12 @@ config TELEMETRY_TEST_QEMU_MODE binary runs under QEMU. Hardware-dependent test cases report TEST_IGNORE instead of crashing on missing peripherals. +config TELEMETRY_TEST_UBSAN + bool "Build telemetry with UndefinedBehaviorSanitizer" + default n + help + Compiles the telemetry component under test with -fsanitize=undefined. + Opt-in: not part of the default build, only enabled by the dedicated + UBSan CI job/sdkconfig overlay. + endmenu diff --git a/car/components/telemetry/test_apps/sdkconfig.defaults.ubsan b/car/components/telemetry/test_apps/sdkconfig.defaults.ubsan new file mode 100644 index 0000000..f75e953 --- /dev/null +++ b/car/components/telemetry/test_apps/sdkconfig.defaults.ubsan @@ -0,0 +1 @@ +CONFIG_TELEMETRY_TEST_UBSAN=y diff --git a/car/components/web_server/test_apps/CMakeLists.txt b/car/components/web_server/test_apps/CMakeLists.txt index 78cfeac..079e315 100644 --- a/car/components/web_server/test_apps/CMakeLists.txt +++ b/car/components/web_server/test_apps/CMakeLists.txt @@ -3,3 +3,11 @@ set(EXTRA_COMPONENT_DIRS "../.." "../../DFRobot_AXP313A/DFRobot_AXP313A/esp_idf" set(COMPONENTS main) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(web_server_test) + +# Opt-in via CONFIG_WEB_SERVER_TEST_UBSAN (see CONTRIBUTING.md); scoped to the +# component under test since enabling UBSan project-wide grows code/data size +# too much to fit on-target. +if(CONFIG_WEB_SERVER_TEST_UBSAN) + idf_component_get_property(web_server_lib web_server COMPONENT_LIB) + target_compile_options(${web_server_lib} PRIVATE "-fsanitize=undefined" "-fno-sanitize=shift-base") +endif() diff --git a/car/components/web_server/test_apps/main/Kconfig.projbuild b/car/components/web_server/test_apps/main/Kconfig.projbuild index acd1197..28fdeb8 100644 --- a/car/components/web_server/test_apps/main/Kconfig.projbuild +++ b/car/components/web_server/test_apps/main/Kconfig.projbuild @@ -8,4 +8,12 @@ config WEB_SERVER_TEST_QEMU_MODE test binary runs under QEMU. Hardware-dependent test cases report TEST_IGNORE instead of blocking on missing peripherals. +config WEB_SERVER_TEST_UBSAN + bool "Build web_server with UndefinedBehaviorSanitizer" + default n + help + Compiles the web_server component under test with -fsanitize=undefined. + Opt-in: not part of the default build, only enabled by the dedicated + UBSan CI job/sdkconfig overlay. + endmenu diff --git a/car/components/web_server/test_apps/sdkconfig.defaults.ubsan b/car/components/web_server/test_apps/sdkconfig.defaults.ubsan new file mode 100644 index 0000000..f82640a --- /dev/null +++ b/car/components/web_server/test_apps/sdkconfig.defaults.ubsan @@ -0,0 +1 @@ +CONFIG_WEB_SERVER_TEST_UBSAN=y