diff --git a/components/heap/Kconfig b/components/heap/Kconfig index c0e226bdd..34c044fc6 100644 --- a/components/heap/Kconfig +++ b/components/heap/Kconfig @@ -63,4 +63,10 @@ menu "Heap memory debugging" This function depends on heap poisoning being enabled and adds four more bytes of overhead for each block allocated. + config HEAP_ABORT_WHEN_ALLOCATION_FAILS + bool "Abort if memory allocation fails" + default n + help + When enabled, if a memory allocation operation fails it will cause a system abort. + endmenu diff --git a/components/heap/heap_caps.c b/components/heap/heap_caps.c index e97bc9d19..7994b709a 100644 --- a/components/heap/heap_caps.c +++ b/components/heap/heap_caps.c @@ -21,6 +21,7 @@ #include "multi_heap.h" #include "esp_log.h" #include "heap_private.h" +#include "esp_system.h" /* This file, combined with a region allocator that supports multiple heaps, solves the problem that the ESP32 has RAM @@ -31,6 +32,8 @@ its knowledge of how the memory is configured along with a priority scheme to al possible. This should optimize the amount of RAM accessible to the code without hardwiring addresses. */ +static esp_alloc_failed_hook_t alloc_failed_callback; + /* This takes a memory chunk in a region that can be addressed as both DRAM as well as IRAM. It will convert it to IRAM in such a way that it can be later freed. It assumes both the address as well as the length to be word-aligned. @@ -53,6 +56,29 @@ IRAM_ATTR static void *dram_alloc_to_iram_addr(void *addr, size_t len) return iptr + 1; } + +static void heap_caps_alloc_failed(size_t requested_size, uint32_t caps, const char *function_name) +{ + if (alloc_failed_callback) { + alloc_failed_callback(requested_size, caps, function_name); + } + + #ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS + esp_system_abort("Memory allocation failed"); + #endif +} + +esp_err_t heap_caps_register_failed_alloc_callback(esp_alloc_failed_hook_t callback) +{ + if (callback == NULL) { + return ESP_ERR_INVALID_ARG; + } + + alloc_failed_callback = callback; + + return ESP_OK; +} + bool heap_caps_match(const heap_t *heap, uint32_t caps) { return heap->heap != NULL && ((get_all_caps(heap) & caps) == caps); @@ -68,6 +94,8 @@ IRAM_ATTR void *heap_caps_malloc( size_t size, uint32_t caps ) if (size > HEAP_SIZE_MAX) { // Avoids int overflow when adding small numbers to size, or // calculating 'end' from start+size, by limiting 'size' to the possible range + heap_caps_alloc_failed(size, caps, __func__); + return NULL; } @@ -77,6 +105,8 @@ IRAM_ATTR void *heap_caps_malloc( size_t size, uint32_t caps ) //NULL directly, even although our heap capabilities (based on soc_memory_tags & soc_memory_regions) would //indicate there is a tag for this. if ((caps & MALLOC_CAP_8BIT) || (caps & MALLOC_CAP_DMA)) { + heap_caps_alloc_failed(size, caps, __func__); + return NULL; } caps |= MALLOC_CAP_32BIT; // IRAM is 32-bit accessible RAM @@ -121,6 +151,9 @@ IRAM_ATTR void *heap_caps_malloc( size_t size, uint32_t caps ) } } } + + heap_caps_alloc_failed(size, caps, __func__); + //Nothing usable found. return NULL; } @@ -288,6 +321,8 @@ IRAM_ATTR void *heap_caps_realloc( void *ptr, size_t size, int caps) } if (size > HEAP_SIZE_MAX) { + heap_caps_alloc_failed(size, caps, __func__); + return NULL; } @@ -342,6 +377,9 @@ IRAM_ATTR void *heap_caps_realloc( void *ptr, size_t size, int caps) heap_caps_free(ptr); return new_p; } + + heap_caps_alloc_failed(size, caps, __func__); + return NULL; } @@ -518,12 +556,15 @@ IRAM_ATTR void *heap_caps_aligned_alloc(size_t alignment, size_t size, int caps) if (size > HEAP_SIZE_MAX) { // Avoids int overflow when adding small numbers to size, or // calculating 'end' from start+size, by limiting 'size' to the possible range + heap_caps_alloc_failed(size, caps, __func__); + return NULL; } //aligned alloc for now only supports default allocator or external //allocator. if((caps & (MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM)) == 0) { + heap_caps_alloc_failed(size, caps, __func__); return NULL; } @@ -550,6 +591,9 @@ IRAM_ATTR void *heap_caps_aligned_alloc(size_t alignment, size_t size, int caps) } } } + + heap_caps_alloc_failed(size, caps, __func__); + //Nothing usable found. return NULL; } diff --git a/components/heap/include/esp_heap_caps.h b/components/heap/include/esp_heap_caps.h index 6ccbdf943..ff303ddf8 100644 --- a/components/heap/include/esp_heap_caps.h +++ b/components/heap/include/esp_heap_caps.h @@ -17,6 +17,7 @@ #include #include "multi_heap.h" #include +#include "esp_err.h" #ifdef __cplusplus extern "C" { @@ -42,6 +43,21 @@ extern "C" { #define MALLOC_CAP_INVALID (1<<31) ///< Memory can't be used / list end marker +/** + * @brief callback called when a allocation operation fails, if registered + * @param size in bytes of failed allocation + * @param caps capabillites requested of failed allocation + * @param function_name function which generated the failure + */ +typedef void (*esp_alloc_failed_hook_t) (size_t size, uint32_t caps, const char * function_name); + +/** + * @brief registers a callback function to be invoked if a memory allocation operation fails + * @param callback caller defined callback to be invoked + * @return ESP_OK if callback was registered. + */ +esp_err_t heap_caps_register_failed_alloc_callback(esp_alloc_failed_hook_t callback); + /** * @brief Allocate a chunk of memory which has the given capabilities * diff --git a/components/heap/test/test_malloc_caps.c b/components/heap/test/test_malloc_caps.c index d081bf2e7..f878955e1 100644 --- a/components/heap/test/test_malloc_caps.c +++ b/components/heap/test/test_malloc_caps.c @@ -182,3 +182,62 @@ TEST_CASE("heap_caps_xxx functions work with flash cache disabled", "[heap]") { TEST_ASSERT( iram_malloc_test() ); } + +#ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS +TEST_CASE("When enabled, allocation operation failure generates an abort", "[heap][reset=abort,SW_CPU_RESET]") +{ + const size_t stupid_allocation_size = (128 * 1024 * 1024); + void *ptr = heap_caps_malloc(stupid_allocation_size, MALLOC_CAP_DEFAULT); + (void)ptr; + TEST_FAIL_MESSAGE("should not be reached"); +} +#endif + +static bool called_user_failed_hook = false; + +void heap_caps_alloc_failed_hook(size_t requested_size, uint32_t caps, const char *function_name) +{ + printf("%s was called but failed to allocate %d bytes with 0x%X capabilities. \n",function_name, requested_size, caps); + called_user_failed_hook = true; +} + +TEST_CASE("user provided alloc failed hook must be called when allocation fails", "[heap]") +{ + TEST_ASSERT(heap_caps_register_failed_alloc_callback(heap_caps_alloc_failed_hook) == ESP_OK); + + const size_t stupid_allocation_size = (128 * 1024 * 1024); + void *ptr = heap_caps_malloc(stupid_allocation_size, MALLOC_CAP_DEFAULT); + TEST_ASSERT(called_user_failed_hook != false); + + called_user_failed_hook = false; + ptr = heap_caps_realloc(ptr, stupid_allocation_size, MALLOC_CAP_DEFAULT); + TEST_ASSERT(called_user_failed_hook != false); + + called_user_failed_hook = false; + ptr = heap_caps_aligned_alloc(0x200, stupid_allocation_size, MALLOC_CAP_DEFAULT); + TEST_ASSERT(called_user_failed_hook != false); + + (void)ptr; +} + +TEST_CASE("allocation with invalid capability should also trigger the alloc failed hook", "[heap]") +{ + const size_t allocation_size = 64; + const uint32_t invalid_cap = MALLOC_CAP_INVALID; + + TEST_ASSERT(heap_caps_register_failed_alloc_callback(heap_caps_alloc_failed_hook) == ESP_OK); + + called_user_failed_hook = false; + void *ptr = heap_caps_malloc(allocation_size, invalid_cap); + TEST_ASSERT(called_user_failed_hook != false); + + called_user_failed_hook = false; + ptr = heap_caps_realloc(ptr, allocation_size, invalid_cap); + TEST_ASSERT(called_user_failed_hook != false); + + called_user_failed_hook = false; + ptr = heap_caps_aligned_alloc(0x200, allocation_size, invalid_cap); + TEST_ASSERT(called_user_failed_hook != false); + + (void)ptr; +} \ No newline at end of file diff --git a/docs/en/api-reference/system/heap_debug.rst b/docs/en/api-reference/system/heap_debug.rst index 1a9cb964b..827e05b54 100644 --- a/docs/en/api-reference/system/heap_debug.rst +++ b/docs/en/api-reference/system/heap_debug.rst @@ -44,6 +44,33 @@ If a heap integrity assertion fails, a line will be printed like ``CORRUPT HEAP: It's also possible to manually check heap integrity by calling :cpp:func:`heap_caps_check_integrity_all` or related functions. This function checks all of requested heap memory for integrity, and can be used even if assertions are disabled. If the integrity check prints an error, it will also contain the address(es) of corrupt heap structures. +Memory Allocation Failed Hook +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Users can use :cpp:func:`heap_caps_register_failed_alloc_callback` to register a callback that will be invoked every time a allocation +operation fails. + +Additionaly user can enable a generation of a system abort if allocation operation fails by following the steps below: +- In the project configuration menu, navigate to ``Component config`` -> ``Heap Memory Debugging`` and select ``Abort if memory allocation fails`` option (see :ref:`CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS`). + +The example below show how to register a allocation failure callback:: + + #include "esp_heap_caps.h" + + void heap_caps_alloc_failed_hook(size_t requested_size, uint32_t caps, const char *function_name) + { + printf("%s was called but failed to allocate %d bytes with 0x%X capabilities. \n",function_name, requested_size, caps); + } + + void app_main() + { + ... + esp_err_t error = heap_caps_register_failed_alloc_callback(heap_caps_alloc_failed_hook); + ... + void *ptr = heap_caps_malloc(allocation_size, MALLOC_CAP_DEFAULT); + ... + } + Finding Heap Corruption ^^^^^^^^^^^^^^^^^^^^^^^