[Concept,31/35] malloc: Record caller backtrace for each allocation

Message ID 20251210000737.180797-32-sjg@u-boot.org
State New
Headers
Series malloc: Add heap debugging commands and mcheck caller tracking |

Commit Message

Simon Glass Dec. 10, 2025, 12:07 a.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

When CONFIG_MCHECK_HEAP_PROTECTION is enabled, use backtrace_str() to
capture the caller information for each malloc/calloc/realloc/memalign
call. This information is stored in the mcheck header and can be viewed
with 'malloc dump'.

Add a flag to disable the backtrace when the stack is corrupted, since
the backtrace code tries to walks the invalid stack frames and will
crash.

Note: A few allocations made during libbacktrace initialisation may
not have caller info since they occur during the first backtrace call.

Example output showing caller info:
    18a1d010   90  used  log_init:453 <-board_init_r:774
    18a1d0a0 6060  used  membuf_new:420 <-console_record
    18a3b840   90  used  of_alias_scan:911 <-board_init_

Fix up the backtrace test to avoid recursion.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 common/dlmalloc.c        | 35 +++++++++++++++++++++++++++++++----
 doc/usage/cmd/malloc.rst | 12 ++++++++++++
 include/malloc.h         | 15 +++++++++++++++
 test/lib/backtrace.c     |  7 ++++---
 4 files changed, 62 insertions(+), 7 deletions(-)
  

Patch

diff --git a/common/dlmalloc.c b/common/dlmalloc.c
index 14515e423cc..c294b355ff2 100644
--- a/common/dlmalloc.c
+++ b/common/dlmalloc.c
@@ -5953,8 +5953,35 @@  size_t dlmalloc_usable_size(const void* mem) {
 }
 
 #if CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION)
+#include <backtrace.h>
 #include "mcheck_core.inc.h"
 
+/* Guard against recursive backtrace calls during malloc */
+static bool in_backtrace __section(".data");
+
+/*
+ * Flag to disable backtrace collection when the stack is known to be corrupt.
+ * Set via malloc_backtrace_skip() before calling panic().
+ */
+static bool mcheck_skip_backtrace __section(".data");
+
+void malloc_backtrace_skip(bool skip)
+{
+	mcheck_skip_backtrace = skip;
+}
+
+static const char *mcheck_caller(void)
+{
+	const char *caller = NULL;
+
+	if (!in_backtrace && !mcheck_skip_backtrace) {
+		in_backtrace = true;
+		caller = backtrace_str(2);
+		in_backtrace = false;
+	}
+	return caller;
+}
+
 void *dlmalloc(size_t bytes)
 {
 	mcheck_pedantic_prehook();
@@ -5963,7 +5990,7 @@  void *dlmalloc(size_t bytes)
 
 	if (!p)
 		return p;
-	return mcheck_alloc_posthook(p, bytes, NULL);
+	return mcheck_alloc_posthook(p, bytes, mcheck_caller());
 }
 
 void dlfree(void *mem) { dlfree_impl(mcheck_free_prehook(mem)); }
@@ -5988,7 +6015,7 @@  void *dlrealloc(void *oldmem, size_t bytes)
 	p = dlrealloc_impl(p, newsz);
 	if (!p)
 		return p;
-	return mcheck_alloc_noclean_posthook(p, bytes, NULL);
+	return mcheck_alloc_noclean_posthook(p, bytes, mcheck_caller());
 }
 
 void *dlmemalign(size_t alignment, size_t bytes)
@@ -5999,7 +6026,7 @@  void *dlmemalign(size_t alignment, size_t bytes)
 
 	if (!p)
 		return p;
-	return mcheck_memalign_posthook(alignment, p, bytes, NULL);
+	return mcheck_memalign_posthook(alignment, p, bytes, mcheck_caller());
 }
 
 /* dlpvalloc, dlvalloc redirect to dlmemalign, so they need no wrapping */
@@ -6013,7 +6040,7 @@  void *dlcalloc(size_t n, size_t elem_size)
 
 	if (!p)
 		return p;
-	return mcheck_alloc_noclean_posthook(p, n * elem_size, NULL);
+	return mcheck_alloc_noclean_posthook(p, n * elem_size, mcheck_caller());
 }
 
 /* mcheck API */
diff --git a/doc/usage/cmd/malloc.rst b/doc/usage/cmd/malloc.rst
index fac9bb29aac..3693034b41e 100644
--- a/doc/usage/cmd/malloc.rst
+++ b/doc/usage/cmd/malloc.rst
@@ -59,6 +59,18 @@  Example
     Used: c2ef0 bytes in 931 chunks
     Free: 5f3f0c0 bytes in 2 chunks + top
 
+With CONFIG_MCHECK_HEAP_PROTECTION enabled, the caller backtrace is shown::
+
+    => malloc dump
+    Heap dump: 18a1d000 - 1ea1f000
+         Address        Size  Status
+    ----------------------------------
+        18a1d000          10  (chunk header)
+        18a1d010          90  used  log_init:453 <-board_init_r:774
+        18a1d0a0        6060  used  membuf_new:420 <-console_record
+        18a3b840          90  used  of_alias_scan:911 <-board_init_
+        ...
+
 Configuration
 -------------
 
diff --git a/include/malloc.h b/include/malloc.h
index a4d588936ec..3327bdcb44f 100644
--- a/include/malloc.h
+++ b/include/malloc.h
@@ -712,6 +712,21 @@  void malloc_disable_testing(void);
  */
 void malloc_dump(void);
 
+/**
+ * malloc_backtrace_skip() - Control backtrace collection in malloc
+ *
+ * When the stack is corrupted (e.g., by a stack overflow), collecting
+ * a backtrace during malloc can crash. Use this function to disable
+ * backtrace collection before corrupting the stack.
+ *
+ * @skip: true to skip backtrace collection, false to enable it
+ */
+#if CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION)
+void malloc_backtrace_skip(bool skip);
+#else
+static inline void malloc_backtrace_skip(bool skip) {}
+#endif
+
 /**
  * mem_malloc_init() - Initialize the malloc() heap
  *
diff --git a/test/lib/backtrace.c b/test/lib/backtrace.c
index 3f20b7854bf..155e5b13af8 100644
--- a/test/lib/backtrace.c
+++ b/test/lib/backtrace.c
@@ -68,16 +68,17 @@  static int lib_test_backtrace_str(struct unit_test_state *uts)
 		 line);
 	ut_asserteq_regex(pattern, str);
 
-	/* Test backtrace_str() */
+	/* Test backtrace_str() - copy result before printf since it may recurse */
 	line = __LINE__ + 1;
 	cstr = backtrace_str(0);
 	ut_assertnonnull(cstr);
+	strlcpy(buf, cstr, sizeof(buf));
 
-	printf("backtrace_str: %s\n", cstr);
+	printf("backtrace_str: %s\n", buf);
 	snprintf(pattern, sizeof(pattern),
 		 "lib_test_backtrace_str:%d <-ut_run_test:\\d+ <-ut_run_test_live_flat:\\d+",
 		 line);
-	ut_asserteq_regex(pattern, cstr);
+	ut_asserteq_regex(pattern, buf);
 
 	return 0;
 }