From 2c0ecc64faf537c8fd140b751dcc1da9290669e2 Mon Sep 17 00:00:00 2001 From: Alexey Lapshin Date: Mon, 27 Jul 2026 13:01:45 +0700 Subject: [PATCH] fix(heap): preserve aligned allocation with KASAN Account for the left redzone when aligning user pointers. Add regression coverage for KASAN-enabled allocations. --- components/heap/heap_caps_base.c | 10 +++++-- .../system/kasan_test/main/kasan_test_main.c | 30 +++++++++++++++++++ .../system/kasan_test/pytest_kasan.py | 10 +++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/components/heap/heap_caps_base.c b/components/heap/heap_caps_base.c index 5727a0cc832..26707082401 100644 --- a/components/heap/heap_caps_base.c +++ b/components/heap/heap_caps_base.c @@ -40,6 +40,10 @@ * The ordering is intentional: user underflows must hit the left redzone * before they can reach the task-tracking block-owner word. * + * For heap_caps_aligned_alloc(), the tlsf align offset must be + * (block-owner size + KASAN_RZ) so the *user* pointer (after both shifts) + * satisfies the requested alignment. + * * Pointer arithmetic macros live in heap_kasan_layout.h. */ @@ -183,6 +187,8 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment } const size_t alloc_size = KASAN_ADD_RZ(size); + /* Align the eventual user pointer (after block-owner + left redzone). */ + const size_t align_offset = MULTI_HEAP_BLOCK_OWNER_SIZE() + KASAN_RZ; for (int prio = 0; prio < SOC_MEMORY_TYPE_NO_PRIOS; prio++) { //Iterate over heaps and check capabilities at this priority @@ -205,7 +211,7 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment //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. 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, align_offset); // int overflow checked above if (ret != NULL) { #if CONFIG_HEAP_TASK_TRACKING heap_caps_update_per_task_info_alloc(heap, @@ -224,7 +230,7 @@ HEAP_IRAM_ATTR NOINLINE_ATTR void *heap_caps_aligned_alloc_base(size_t alignment } else { //Just try to alloc, nothing special. ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(alloc_size), - alignment, MULTI_HEAP_BLOCK_OWNER_SIZE()); + alignment, align_offset); if (ret != NULL) { #if CONFIG_HEAP_TASK_TRACKING heap_caps_update_per_task_info_alloc(heap, diff --git a/tools/test_apps/system/kasan_test/main/kasan_test_main.c b/tools/test_apps/system/kasan_test/main/kasan_test_main.c index 5c9a27e1230..aba4f76f57e 100644 --- a/tools/test_apps/system/kasan_test/main/kasan_test_main.c +++ b/tools/test_apps/system/kasan_test/main/kasan_test_main.c @@ -15,10 +15,12 @@ */ #include +#include #include #include #include "sdkconfig.h" #include "unity.h" +#include "esp_heap_caps.h" #include "esp_log.h" #include "esp_kasan.h" @@ -139,6 +141,34 @@ TEST_CASE("no false positive", "[kasan]") ESP_LOGI(TAG, "no-bug test PASSED"); } +/* ---------------------------------------------------------------------- */ + +TEST_CASE("aligned allocation preserves alignment", "[kasan][heap]") +{ + static const size_t alignments[] = { 8, 16, 32, 64 }; + const uint32_t alloc_size = 37; + +#if CONFIG_KASAN_NO_HALT + kasan_reset_error_count(); +#endif + + for (size_t i = 0; i < sizeof(alignments) / sizeof(alignments[0]); i++) { + const size_t alignment = alignments[i]; + uint8_t *buf = heap_caps_aligned_alloc(alignment, alloc_size, MALLOC_CAP_DEFAULT); + + TEST_ASSERT_NOT_NULL(buf); + TEST_ASSERT_EQUAL_UINT32(0, (uintptr_t)buf & (alignment - 1)); + /* Verify the full user region remains accessible after the KASAN redzone offset. */ + memset(buf, 0xA5, alloc_size); + heap_caps_free(buf); + } + +#if CONFIG_KASAN_NO_HALT + TEST_ASSERT_EQUAL_UINT32(0, kasan_get_error_count()); +#endif + ESP_LOGI(TAG, "aligned allocation test PASSED"); +} + /* ---------------------------------------------------------------------- */ /* * GCC -fsanitize=kernel-address instrumentation calls a sized family of diff --git a/tools/test_apps/system/kasan_test/pytest_kasan.py b/tools/test_apps/system/kasan_test/pytest_kasan.py index 7026fa0d4fb..9cfbbf49f3f 100644 --- a/tools/test_apps/system/kasan_test/pytest_kasan.py +++ b/tools/test_apps/system/kasan_test/pytest_kasan.py @@ -105,6 +105,16 @@ def test_kasan_halt_no_false_positive(dut: Dut) -> None: 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_aligned_alloc(dut: Dut) -> None: + """KASAN redzones must not change aligned allocator guarantees.""" + dut.expect('KASAN test application starting', timeout=15) + dut.write('"aligned allocation preserves alignment"') + dut.expect('aligned allocation test PASSED', timeout=15) + + @pytest.mark.generic @idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target']) @pytest.mark.parametrize('config', ['halt'], indirect=True)