From 36147a738d0da5ca10310f59aed2ed45256b275f Mon Sep 17 00:00:00 2001 From: Renz Bagaporo Date: Wed, 17 Jun 2026 13:27:54 +0900 Subject: [PATCH 1/3] ci(heap): add task tracking realloc handle reuse test --- .../heap_tests/main/test_task_tracking.c | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/components/heap/test_apps/heap_tests/main/test_task_tracking.c b/components/heap/test_apps/heap_tests/main/test_task_tracking.c index a0ff1974761..7dad2a28e39 100644 --- a/components/heap/test_apps/heap_tests/main/test_task_tracking.c +++ b/components/heap/test_apps/heap_tests/main/test_task_tracking.c @@ -6,12 +6,15 @@ #include "unity.h" #include "stdio.h" #include +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_heap_caps.h" #include "esp_heap_task_info.h" +extern void set_leak_threshold(int threshold); + // This test only apply when task tracking is enabled #if defined(CONFIG_HEAP_TASK_TRACKING) && defined(CONFIG_HEAP_TRACK_DELETED_TASKS) @@ -248,4 +251,135 @@ TEST_CASE("heap task tracking check alloc arrays and get info on specific task", vTaskDelete(test_task_handle); } +typedef struct { + void *ptr; + TaskHandle_t task; +} handle_reuse_allocation_t; + +#define NUM_HANDLE_REUSE_ATTEMPTS 500 + +static volatile handle_reuse_allocation_t s_handle_reuse_allocations[NUM_HANDLE_REUSE_ATTEMPTS]; +static volatile bool s_handle_reuse_found = false; +static volatile TaskHandle_t s_reused_task_handle = NULL; +static volatile bool s_alloc_task_done = false; + +static bool task_usage_underflowed(size_t usage) +{ + /* size_t underflow looks negative when printed with %d (e.g. -16 -> 0xFFFFFFF0). */ + return usage > ((size_t)INT32_MAX); +} + +static void assert_no_underflow_for_reused_handle(TaskHandle_t handle) +{ + heap_all_tasks_stat_t tasks_stat; + esp_err_t ret_val = heap_caps_alloc_all_task_stat_arrays(&tasks_stat); + TEST_ASSERT_EQUAL(ESP_OK, ret_val); + ret_val = heap_caps_get_all_task_stat(&tasks_stat); + TEST_ASSERT_EQUAL(ESP_OK, ret_val); + + size_t matching = 0; + for (size_t task_index = 0; task_index < tasks_stat.task_count; task_index++) { + task_stat_t task_stat = tasks_stat.stat_arr[task_index]; + if (task_stat.handle != handle) { + continue; + } + + matching++; + TEST_ASSERT_FALSE_MESSAGE(task_usage_underflowed(task_stat.overall_current_usage), + "overall_current_usage underflowed"); + for (size_t heap_index = 0; heap_index < task_stat.heap_count; heap_index++) { + TEST_ASSERT_FALSE_MESSAGE(task_usage_underflowed(task_stat.heap_stat[heap_index].current_usage), + "heap current_usage underflowed"); + } + } + + TEST_ASSERT_GREATER_THAN(0, matching); + heap_caps_free_all_task_stat_arrays(&tasks_stat); +} + +static void realloc_then_free(void *ptr, size_t size) +{ + void *new_ptr = heap_caps_realloc(ptr, size, MALLOC_CAP_DEFAULT); + if (new_ptr != NULL) { + ptr = new_ptr; + } + heap_caps_free(ptr); +} + +static void handle_reuse_dummy_task(void *args) +{ + (void)args; + while (1) { + taskYIELD(); + } +} + +static void handle_reuse_alloc_task(void *args) +{ + TaskHandle_t handle = xTaskGetCurrentTaskHandle(); + size_t i = (size_t)args; + + s_handle_reuse_allocations[i].ptr = heap_caps_malloc(10, MALLOC_CAP_DEFAULT); + if (s_handle_reuse_allocations[i].ptr == NULL) { + abort(); + } + s_handle_reuse_allocations[i].task = handle; + + for (size_t j = 0; j < i; j++) { + if (s_handle_reuse_allocations[j].task == handle) { + /* Same TaskHandle_t as a previously deleted task: exercise the realloc path. */ + realloc_then_free(s_handle_reuse_allocations[i].ptr, 20); + realloc_then_free(s_handle_reuse_allocations[j].ptr, 20); + s_handle_reuse_allocations[i].ptr = NULL; + s_handle_reuse_allocations[j].ptr = NULL; + + s_handle_reuse_found = true; + s_reused_task_handle = handle; + s_alloc_task_done = true; + vTaskDelete(NULL); + return; + } + } + + s_alloc_task_done = true; + vTaskDelete(NULL); +} + +/* Reproduce the scenario from GoeBachmann/esp-idf (heap-tracking-bug branch), + * examples/system/heap_task_tracking/basic: create many short-lived tasks until + * a new task reuses a deleted task's TaskHandle_t, then realloc/free allocations + * from both lifetimes. Without the fix, overall_current_usage underflows and + * appears negative in logs. + */ +TEST_CASE("heap task tracking realloc with reused TaskHandle does not underflow usage", "[heap]") +{ + TaskHandle_t dummy_task_handle = NULL; + + set_leak_threshold(-50000); + + s_handle_reuse_found = false; + s_reused_task_handle = NULL; + memset((void *)s_handle_reuse_allocations, 0, sizeof(s_handle_reuse_allocations)); + + xTaskCreate(&handle_reuse_dummy_task, "dummy_task", 3072, NULL, 0, &dummy_task_handle); + + for (size_t i = 0; i < NUM_HANDLE_REUSE_ATTEMPTS; i++) { + s_alloc_task_done = false; + xTaskCreate(&handle_reuse_alloc_task, "alloc_task", 3072, (void *)i, 5, NULL); + while (!s_alloc_task_done) { + vTaskDelay(pdMS_TO_TICKS(50)); + } + + if (s_handle_reuse_found) { + break; + } + } + + TEST_ASSERT_TRUE_MESSAGE(s_handle_reuse_found, + "Could not force TaskHandle reuse within iteration limit"); + assert_no_underflow_for_reused_handle(s_reused_task_handle); + + vTaskDelete(dummy_task_handle); +} + #endif // CONFIG_HEAP_TASK_TRACKING From a033f2ad49d92a91861870bce92ed9873fa85b83 Mon Sep 17 00:00:00 2001 From: Ferdinand Bachmann Date: Mon, 15 Jun 2026 14:46:33 +0200 Subject: [PATCH 2/3] fix(heap_task_info): Fix incorrect current_usage subtraction on realloc() --- components/heap/heap_task_info.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/components/heap/heap_task_info.c b/components/heap/heap_task_info.c index c4a1c277698..30c168e6bbf 100644 --- a/components/heap/heap_task_info.c +++ b/components/heap/heap_task_info.c @@ -338,11 +338,8 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *o SLIST_FOREACH(task_info, &task_stats, next_task_info) { if (task_info->task_stat.handle == old_task) { heap_stats_t *heap_stats = NULL; - task_info->task_stat.overall_current_usage -= old_size; STAILQ_FOREACH(heap_stats, &task_info->heaps_stats, next_heap_stat) { if (heap_stats->heap == heap->heap) { - heap_stats->heap_stat.current_usage -= old_size; - heap_stats->heap_stat.alloc_count--; /* remove the alloc from the list. The updated alloc stats are added later * in the function */ @@ -354,6 +351,13 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *o break; } } + + if (alloc_stat != NULL) { + heap_stats->heap_stat.current_usage -= old_size; + heap_stats->heap_stat.alloc_count--; + task_info->task_stat.overall_current_usage -= old_size; + } + break; } } @@ -430,6 +434,8 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_free(heap_t *heap, void *ptr) heap_stats->heap_stat.current_usage -= alloc_stat->alloc_stat.size; task_info->task_stat.overall_current_usage -= alloc_stat->alloc_stat.size; } + + break; } } From 858675b470356cc7a334aebb171ef5ccab39c25f Mon Sep 17 00:00:00 2001 From: Renz Bagaporo Date: Fri, 19 Jun 2026 10:17:33 +0900 Subject: [PATCH 3/3] fix(heap): reset alloc_stat before realloc lookup Closes https://github.com/espressif/esp-idf/issues/18723 Closes https://github.com/espressif/esp-idf/pull/18724 --- components/heap/heap_task_info.c | 1 + 1 file changed, 1 insertion(+) diff --git a/components/heap/heap_task_info.c b/components/heap/heap_task_info.c index 30c168e6bbf..e992040dccb 100644 --- a/components/heap/heap_task_info.c +++ b/components/heap/heap_task_info.c @@ -343,6 +343,7 @@ HEAP_IRAM_ATTR void heap_caps_update_per_task_info_realloc(heap_t *heap, void *o /* remove the alloc from the list. The updated alloc stats are added later * in the function */ + alloc_stat = NULL; STAILQ_FOREACH(alloc_stat, &heap_stats->allocs_stats, next_alloc_stat) { if (alloc_stat->alloc_stat.address == old_ptr) { STAILQ_REMOVE(&heap_stats->allocs_stats, alloc_stat, alloc_stats, next_alloc_stat);