From: Simon Glass <simon.glass@canonical.com> Add a malloc()-traffic log that records all malloc()-related calls with their addresses, sizes, and caller information. This is useful for debugging allocation patterns and finding the source of allocations that lack caller info in heap dumps. Each entry stores: - Operation type (alloc/free/realloc/memalign) - Pointer address - Size (and old size for realloc) - Full caller backtrace string On sandbox, the log buffer is allocated from host memory using os_malloc(), so it does not affect U-Boot's heap. The size is controlled by CONFIG_MCHECK_LOG_SIZE (default 512K entries). If the log fills up, it wraps around (circular buffer) and a warning is shown when dumping to indicate how many entries were lost. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- Kconfig | 22 +++++ common/dlmalloc.c | 198 ++++++++++++++++++++++++++++++++++++++--- doc/develop/malloc.rst | 14 +++ include/malloc.h | 72 +++++++++++++++ test/common/malloc.c | 56 ++++++++++++ 5 files changed, 350 insertions(+), 12 deletions(-) diff --git a/Kconfig b/Kconfig index 3b89f2c48dd..d47077d43bc 100644 --- a/Kconfig +++ b/Kconfig @@ -366,6 +366,28 @@ config MCHECK_CALLER_LEN Larger values provide more context but increase memory overhead per allocation. +config MCHECK_LOG + bool "Enable malloc traffic log" + depends on MCHECK_HEAP_PROTECTION && SANDBOX + default y + help + Enable a log of all malloc()/free()/realloc() calls. This records + each call with its address, size, and caller backtrace. Useful for + debugging allocation patterns and finding the source of memory + leaks. The log uses host memory so it does not affect U-Boot's + heap. + +config MCHECK_LOG_SIZE + int "Number of entries in malloc traffic log" + depends on MCHECK_LOG + default 65536 + help + Sets the number of entries in the malloc traffic log. Each entry + records a malloc()/free()/realloc() call with address, size, and + caller info. The log uses host memory (os_malloc()) so it does not + affect U-Boot's heap. Use 'malloc log start' to begin logging and + 'malloc log' to dump the log. + config SPL_SYS_MALLOC_F bool "Enable malloc() pool in SPL" depends on SPL_FRAMEWORK && SYS_MALLOC_F && SPL diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 63792104ee3..bf60e3004d4 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -5957,6 +5957,7 @@ STATIC_IF_MCHECK size_t dlmalloc_usable_size_impl(const void *mem) #if CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) #include <backtrace.h> +#include <os.h> #include "mcheck_core.inc.h" /* Guard against recursive backtrace calls during malloc */ @@ -5993,8 +5994,152 @@ static const char *mcheck_caller(void) return caller; } +#if CONFIG_IS_ENABLED(MCHECK_LOG) +/* Malloc traffic logging for debugging allocation patterns */ + +#if IS_ENABLED(CONFIG_SANDBOX) +#define MLOG_SIZE CONFIG_MCHECK_LOG_SIZE +#else +#define MLOG_SIZE 1024 +#endif + +/** + * struct mlog_hdr - Header for the malloc traffic log + * + * @buf: Array of log entries + * @size: Number of entries in @buf + * @head: Index of next entry to write + * @count: Total number of entries written (may exceed @size if wrapped) + * @enabled: true if logging is active + */ +struct mlog_hdr { + struct mlog_entry *buf; + uint size; + uint head; + uint count; + bool enabled; +}; + +static struct mlog_hdr mlog __section(".data"); + +static void mlog_record(enum mlog_type type, void *ptr, size_t size, + size_t old_size, const char *caller) +{ + struct mlog_entry *ent; + + if (!mlog.enabled || !mlog.buf) + return; + + ent = &mlog.buf[mlog.head]; + ent->type = type; + ent->ptr = ptr; + ent->size = size; + ent->old_size = old_size; + if (caller) + strlcpy(ent->caller, caller, MLOG_CALLER_LEN); + else + ent->caller[0] = '\0'; + mlog.head = (mlog.head + 1) % mlog.size; + mlog.count++; +} + +void malloc_log_start(void) +{ + if (!mlog.buf) { + if (IS_ENABLED(CONFIG_SANDBOX)) { + mlog.buf = os_malloc(MLOG_SIZE * + sizeof(struct mlog_entry)); + if (!mlog.buf) { + printf("Failed to allocate malloc log buffer\n"); + return; + } + mlog.size = MLOG_SIZE; + } else { + printf("Malloc log not available on this platform\n"); + return; + } + } + memset(mlog.buf, '\0', mlog.size * sizeof(struct mlog_entry)); + mlog.head = 0; + mlog.count = 0; + mlog.enabled = true; +} + +void malloc_log_stop(void) +{ + mlog.enabled = false; +} + +int malloc_log_info(struct mlog_info *info) +{ + if (!mlog.buf) + return -ENOENT; + + if (mlog.count > mlog.size) + info->entry_count = mlog.size; + else + info->entry_count = mlog.count; + info->max_entries = mlog.size; + info->total_count = mlog.count; + + return 0; +} + +int malloc_log_entry(uint idx, struct mlog_entry **entryp) +{ + uint start, count; + + if (!mlog.buf) + return -ENOENT; + + if (mlog.count > mlog.size) { + count = mlog.size; + start = mlog.head; + } else { + count = mlog.count; + start = 0; + } + + if (idx >= count) + return -ERANGE; + + *entryp = &mlog.buf[(start + idx) % mlog.size]; + + return 0; +} + +#else /* !MCHECK_LOG */ + +static inline void mlog_record(enum mlog_type type, void *ptr, size_t size, + size_t old_size, const char *caller) +{ +} + +void malloc_log_start(void) +{ +} + +void malloc_log_stop(void) +{ +} + +int malloc_log_info(struct mlog_info *info) +{ + return -ENOENT; +} + +int malloc_log_entry(uint idx, struct mlog_entry **entryp) +{ + return -ENOENT; +} + +#endif /* MCHECK_LOG */ + void *dlmalloc(size_t bytes) { + const char *caller; + void *p; + /* * Skip mcheck for simple malloc (pre-relocation). Simple malloc is a * bump allocator that can't free, so mcheck overhead is useless and @@ -6007,13 +6152,17 @@ void *dlmalloc(size_t bytes) if (mcheck_disabled) return dlmalloc_impl(bytes CALLER_NULL); + caller = mcheck_caller(); mcheck_pedantic_prehook(); size_t fullsz = mcheck_alloc_prehook(bytes); - void *p = dlmalloc_impl(fullsz CALLER_NULL); + p = dlmalloc_impl(fullsz CALLER_NULL); if (!p) return p; - return mcheck_alloc_posthook(p, bytes, mcheck_caller()); + p = mcheck_alloc_posthook(p, bytes, caller); + mlog_record(MLOG_ALLOC, p, bytes, 0, caller); + + return p; } void dlfree(void *mem) @@ -6027,11 +6176,16 @@ void dlfree(void *mem) dlfree_impl(mem); return; } + mlog_record(MLOG_FREE, mem, 0, 0, mcheck_caller()); dlfree_impl(mcheck_free_prehook(mem)); } void *dlrealloc(void *oldmem, size_t bytes) { + const char *caller; + size_t old_size; + void *p; + #ifdef REALLOC_ZERO_BYTES_FREES if (bytes == 0) { if (oldmem) @@ -6046,18 +6200,26 @@ void *dlrealloc(void *oldmem, size_t bytes) if (mcheck_disabled) return dlrealloc_impl(oldmem, bytes); + caller = mcheck_caller(); + old_size = dlmalloc_usable_size(oldmem); mcheck_pedantic_prehook(); - void *p = mcheck_reallocfree_prehook(oldmem); + p = mcheck_reallocfree_prehook(oldmem); size_t newsz = mcheck_alloc_prehook(bytes); p = dlrealloc_impl(p, newsz); if (!p) return p; - return mcheck_alloc_noclean_posthook(p, bytes, mcheck_caller()); + p = mcheck_alloc_noclean_posthook(p, bytes, caller); + mlog_record(MLOG_REALLOC, p, bytes, old_size, caller); + + return p; } void *dlmemalign(size_t alignment, size_t bytes) { + const char *caller; + void *p; + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) return memalign_simple(alignment, bytes); @@ -6065,24 +6227,31 @@ void *dlmemalign(size_t alignment, size_t bytes) if (mcheck_disabled) return dlmemalign_impl(alignment, bytes); + caller = mcheck_caller(); mcheck_pedantic_prehook(); size_t fullsz = mcheck_memalign_prehook(alignment, bytes); - void *p = dlmemalign_impl(alignment, fullsz); + p = dlmemalign_impl(alignment, fullsz); if (!p) return p; - return mcheck_memalign_posthook(alignment, p, bytes, mcheck_caller()); + p = mcheck_memalign_posthook(alignment, p, bytes, caller); + mlog_record(MLOG_MEMALIGN, p, bytes, 0, caller); + + return p; } /* dlpvalloc, dlvalloc redirect to dlmemalign, so they need no wrapping */ void *dlcalloc(size_t n, size_t elem_size) { + const char *caller; + size_t sz; + void *p; + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) { - size_t sz = n * elem_size; - void *p = malloc_simple(sz); - + sz = n * elem_size; + p = malloc_simple(sz); if (p) memset(p, '\0', sz); return p; @@ -6091,14 +6260,19 @@ void *dlcalloc(size_t n, size_t elem_size) if (mcheck_disabled) return dlcalloc_impl(n, elem_size); + sz = n * elem_size; + caller = mcheck_caller(); mcheck_pedantic_prehook(); /* NB: no overflow check here */ - size_t fullsz = mcheck_alloc_prehook(n * elem_size); - void *p = dlcalloc_impl(1, fullsz); + size_t fullsz = mcheck_alloc_prehook(sz); + p = dlcalloc_impl(1, fullsz); if (!p) return p; - return mcheck_alloc_noclean_posthook(p, n * elem_size, mcheck_caller()); + p = mcheck_alloc_noclean_posthook(p, sz, caller); + mlog_record(MLOG_ALLOC, p, sz, 0, caller); + + return p; } size_t dlmalloc_usable_size(const void *mem) diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst index 8dba2d98afe..92180af055a 100644 --- a/doc/develop/malloc.rst +++ b/doc/develop/malloc.rst @@ -385,6 +385,20 @@ allocation was made:: This caller information makes it easy to track down memory leaks by showing exactly where each allocation originated. +Malloc-Traffic Log +~~~~~~~~~~~~~~~~~~ + +On sandbox, when mcheck is enabled, a malloc-traffic log can record all +malloc/free/realloc calls. This is useful for debugging allocation patterns +and finding where allocations without caller info originate. + +The log buffer is allocated from host memory using ``os_malloc()``, so it +does not affect U-Boot's heap. The size is controlled by +``CONFIG_MCHECK_LOG_SIZE`` (default 512K entries, about 80MB). + +If the log fills up, it wraps around and overwrites the oldest entries. +A warning is shown when dumping if entries were lost. + Valgrind ~~~~~~~~ diff --git a/include/malloc.h b/include/malloc.h index 3327bdcb44f..f8bfe843738 100644 --- a/include/malloc.h +++ b/include/malloc.h @@ -712,6 +712,78 @@ void malloc_disable_testing(void); */ void malloc_dump(void); +/** + * malloc_log_start() - Start logging malloc traffic + * + * Enables recording of malloc/free/realloc calls to a circular buffer. + * Use malloc_log_dump() to print the recorded entries. + */ +void malloc_log_start(void); + +/** + * malloc_log_stop() - Stop logging malloc traffic + */ +void malloc_log_stop(void); + +/** + * enum mlog_type - Type of malloc log entry + */ +enum mlog_type { + MLOG_ALLOC, + MLOG_FREE, + MLOG_REALLOC, + MLOG_MEMALIGN, +}; + +#define MLOG_CALLER_LEN 128 + +/** + * struct mlog_entry - Entry in the malloc traffic log + * + * @type: Operation type (alloc, free, realloc, memalign) + * @ptr: Pointer returned by or passed to the operation + * @size: Size of allocation (0 for free) + * @old_size: Previous size for realloc, 0 otherwise + * @caller: Backtrace string showing where the call originated + */ +struct mlog_entry { + enum mlog_type type; + void *ptr; + size_t size; + size_t old_size; + char caller[MLOG_CALLER_LEN]; +}; + +/** + * struct mlog_info - Information about the malloc log + * + * @entry_count: Number of entries currently available in the log + * @max_entries: Maximum number of entries the log can hold + * @total_count: Total number of entries recorded (may exceed max if wrapped) + */ +struct mlog_info { + uint entry_count; + uint max_entries; + uint total_count; +}; + +/** + * malloc_log_info() - Get information about the malloc log + * + * @info: Returns log statistics + * Return: 0 on success, -ENOENT if log not started + */ +int malloc_log_info(struct mlog_info *info); + +/** + * malloc_log_entry() - Get a specific entry from the malloc log + * + * @idx: Index of entry to retrieve (0 = oldest available) + * @entryp: Returns pointer to the log entry + * Return: 0 on success, -ENOENT if log not started, -ERANGE if idx out of range + */ +int malloc_log_entry(uint idx, struct mlog_entry **entryp); + /** * malloc_backtrace_skip() - Control backtrace collection in malloc * diff --git a/test/common/malloc.c b/test/common/malloc.c index 436aac503be..4e5bd68c013 100644 --- a/test/common/malloc.c +++ b/test/common/malloc.c @@ -637,3 +637,59 @@ static int common_test_malloc_fill_pool(struct unit_test_state *uts) return 0; } COMMON_TEST(common_test_malloc_fill_pool, 0); + +#if CONFIG_IS_ENABLED(MCHECK_LOG) +/* Test malloc_log_info() and malloc_log_entry() */ +static int common_test_malloc_log_info(struct unit_test_state *uts) +{ + struct mlog_entry *entry; + struct mlog_info info; + void *ptr, *ptr2; + + /* Should fail before log is started */ + ut_asserteq(-ENOENT, malloc_log_info(&info)); + ut_asserteq(-ENOENT, malloc_log_entry(0, &entry)); + + malloc_log_start(); + + /* Do an allocation, realloc, and free */ + ptr = malloc(0x100); + ut_assertnonnull(ptr); + + ptr2 = realloc(ptr, 0x200); + ut_assertnonnull(ptr2); + + free(ptr2); + + malloc_log_stop(); + + ut_assertok(malloc_log_info(&info)); + ut_asserteq(3, info.entry_count); + ut_assert(info.max_entries > 0); + ut_asserteq(info.entry_count, info.total_count); /* no wrapping */ + + /* Check the alloc entry */ + ut_assertok(malloc_log_entry(0, &entry)); + ut_asserteq(MLOG_ALLOC, entry->type); + ut_asserteq_ptr(ptr, entry->ptr); + ut_asserteq(0x100, entry->size); + + /* Check the realloc entry */ + ut_assertok(malloc_log_entry(1, &entry)); + ut_asserteq(MLOG_REALLOC, entry->type); + ut_asserteq_ptr(ptr2, entry->ptr); + ut_asserteq(0x200, entry->size); + ut_asserteq(0x100, entry->old_size); + + /* Check the free entry */ + ut_assertok(malloc_log_entry(2, &entry)); + ut_asserteq(MLOG_FREE, entry->type); + ut_asserteq_ptr(ptr2, entry->ptr); + + /* Out of range should fail */ + ut_asserteq(-ERANGE, malloc_log_entry(3, &entry)); + + return 0; +} +COMMON_TEST(common_test_malloc_log_info, 0); +#endif /* MCHECK_LOG */ -- 2.43.0