fix(heap): preserve aligned allocation with KASAN

Account for the left redzone when aligning user pointers.
Add regression coverage for KASAN-enabled allocations.
This commit is contained in:
Alexey Lapshin
2026-07-27 13:01:45 +07:00
parent 281d61f131
commit 2c0ecc64fa
3 changed files with 48 additions and 2 deletions

View File

@@ -40,6 +40,10 @@
* The ordering is intentional: user underflows must hit the left redzone * The ordering is intentional: user underflows must hit the left redzone
* before they can reach the task-tracking block-owner word. * 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. * 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); 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++) { for (int prio = 0; prio < SOC_MEMORY_TYPE_NO_PRIOS; prio++) {
//Iterate over heaps and check capabilities at this priority //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 //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. //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, 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 (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING #if CONFIG_HEAP_TASK_TRACKING
heap_caps_update_per_task_info_alloc(heap, 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 { } else {
//Just try to alloc, nothing special. //Just try to alloc, nothing special.
ret = aligned_or_unaligned_alloc(heap->heap, MULTI_HEAP_ADD_BLOCK_OWNER_SIZE(alloc_size), 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 (ret != NULL) {
#if CONFIG_HEAP_TASK_TRACKING #if CONFIG_HEAP_TASK_TRACKING
heap_caps_update_per_task_info_alloc(heap, heap_caps_update_per_task_info_alloc(heap,

View File

@@ -15,10 +15,12 @@
*/ */
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include "sdkconfig.h" #include "sdkconfig.h"
#include "unity.h" #include "unity.h"
#include "esp_heap_caps.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_kasan.h" #include "esp_kasan.h"
@@ -139,6 +141,34 @@ TEST_CASE("no false positive", "[kasan]")
ESP_LOGI(TAG, "no-bug test PASSED"); 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 * GCC -fsanitize=kernel-address instrumentation calls a sized family of

View File

@@ -105,6 +105,16 @@ def test_kasan_halt_no_false_positive(dut: Dut) -> None:
dut.expect('no-bug test PASSED', timeout=15) 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 @pytest.mark.generic
@idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target']) @idf_parametrize('target', ['supported_targets', 'preview_targets'], indirect=['target'])
@pytest.mark.parametrize('config', ['halt'], indirect=True) @pytest.mark.parametrize('config', ['halt'], indirect=True)