[Concept,09/17] cmd: malloc: Add leak subcommand

Message ID 20260316183050.3855921-10-sjg@u-boot.org
State New
Headers
Series Add automatic memory-leak detection to U-Boot tests |

Commit Message

Simon Glass March 16, 2026, 6:30 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Add 'malloc leak' subcommands for interactive heap-leak detection at the
U-Boot command line:

  malloc leak start - snapshot current heap allocations
  malloc leak       - count new allocations (keeps the snapshot)
  malloc leak end   - print and count new allocations, free snapshot

Each leaked allocation is printed with its address, size and caller
backtrace (when mcheck is enabled).

Guarded by CONFIG_CMD_MALLOC_LEAK which depends on CONFIG_SANDBOX since
the snapshot uses os_malloc() for storage.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 cmd/Kconfig            | 10 +++++++
 cmd/malloc.c           | 65 ++++++++++++++++++++++++++++++++++++++++++
 doc/develop/malloc.rst | 32 +++++++++++++++++++--
 test/cmd/malloc.c      | 52 +++++++++++++++++++++++++++++++++
 4 files changed, 157 insertions(+), 2 deletions(-)
  

Patch

diff --git a/cmd/Kconfig b/cmd/Kconfig
index 4af032233b9..f9d6ea46566 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -3139,6 +3139,16 @@  config CMD_MALLOC
 	  about memory allocation, such as total memory allocated and
 	  currently in use.
 
+config CMD_MALLOC_LEAK
+	bool "malloc leak - Detect heap leaks"
+	depends on CMD_MALLOC && SANDBOX
+	default y
+	help
+	  This adds the 'malloc leak' subcommand which snapshots the heap
+	  and later reports any new allocations. Use 'malloc leak start'
+	  before an operation and 'malloc leak end' afterwards to see
+	  leaked allocations with their caller backtraces.
+
 config CMD_MALLOC_LOG
 	bool "malloc log - Log malloc traffic"
 	depends on CMD_MALLOC && MCHECK_LOG
diff --git a/cmd/malloc.c b/cmd/malloc.c
index e7362a39bd2..6b019f4c056 100644
--- a/cmd/malloc.c
+++ b/cmd/malloc.c
@@ -62,6 +62,69 @@  static int __maybe_unused do_malloc_log(struct cmd_tbl *cmdtp, int flag,
 	return 0;
 }
 
+static struct malloc_leak_snap leak_snap;
+
+static int __maybe_unused do_malloc_leak(struct cmd_tbl *cmdtp, int flag,
+					 int argc, char *const argv[])
+{
+	if (argc < 2) {
+		int leaks;
+
+		if (!leak_snap.addr) {
+			printf("No snapshot taken, use 'malloc leak start'\n");
+			return CMD_RET_FAILURE;
+		}
+
+		leaks = malloc_leak_check_count(&leak_snap);
+		if (leaks > 0)
+			printf("%d new allocs\n", leaks);
+		else
+			printf("No leaks\n");
+
+		return 0;
+	}
+
+	if (!strcmp(argv[1], "start")) {
+		int ret;
+
+		if (leak_snap.addr)
+			malloc_leak_check_free(&leak_snap);
+		ret = malloc_leak_check_start(&leak_snap);
+		if (ret)
+			return CMD_RET_FAILURE;
+		printf("Heap snapshot: %d allocs\n", leak_snap.count);
+	} else if (!strcmp(argv[1], "end")) {
+		int leaks;
+
+		if (!leak_snap.addr) {
+			printf("No snapshot taken, use 'malloc leak start'\n");
+			return CMD_RET_FAILURE;
+		}
+
+		leaks = malloc_leak_check_end(&leak_snap);
+		if (leaks > 0)
+			printf("%d leaked allocs\n", leaks);
+		else
+			printf("No leaks\n");
+	} else {
+		return CMD_RET_USAGE;
+	}
+
+	return 0;
+}
+
+#if CONFIG_IS_ENABLED(CMD_MALLOC_LEAK)
+#define MALLOC_LEAK_HELP \
+	"malloc leak [start|end] - detect heap leaks\n" \
+	"    start - snapshot current heap allocations\n" \
+	"    end   - show allocations not in the snapshot\n" \
+	"    (none) - count new allocations without freeing snapshot\n"
+#define MALLOC_LEAK_SUBCMD , U_BOOT_SUBCMD_MKENT(leak, 3, 1, do_malloc_leak)
+#else
+#define MALLOC_LEAK_HELP
+#define MALLOC_LEAK_SUBCMD
+#endif
+
 #if CONFIG_IS_ENABLED(CMD_MALLOC_LOG)
 #define MALLOC_LOG_HELP	\
 	"malloc log [start|stop|dump] - log malloc traffic\n" \
@@ -77,9 +140,11 @@  static int __maybe_unused do_malloc_log(struct cmd_tbl *cmdtp, int flag,
 U_BOOT_LONGHELP(malloc,
 	"info - display malloc statistics\n"
 	"malloc dump - dump heap chunks (address, size, status)\n"
+	MALLOC_LEAK_HELP
 	MALLOC_LOG_HELP);
 
 U_BOOT_CMD_WITH_SUBCMDS(malloc, "malloc information", malloc_help_text,
 	U_BOOT_SUBCMD_MKENT(info, 1, 1, do_malloc_info),
 	U_BOOT_SUBCMD_MKENT(dump, 1, 1, do_malloc_dump)
+	MALLOC_LEAK_SUBCMD
 	MALLOC_LOG_SUBCMD);
diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst
index 6bb17dca7b1..ecaf8af3c3e 100644
--- a/doc/develop/malloc.rst
+++ b/doc/develop/malloc.rst
@@ -527,12 +527,40 @@  This makes it easy to scan an entire test suite for leaks::
 
     $ um t --leak-check -V dm
 
+**Interactive leak checking with the malloc command**
+
+The ``malloc leak`` command provides interactive leak detection at the
+U-Boot command line. Take a snapshot before an operation and check
+afterwards::
+
+    => malloc leak start
+    Heap snapshot: 974 allocs
+    => setenv foo bar
+    => malloc leak end
+      14a2a9a0 90 sandbox_strdup:353 <-hsearch_r:403 <-env_do_env_set:130
+      14a2aa30 90 sandbox_strdup:353 <-hsearch_r:403 <-env_do_env_set:130
+    2 leaked allocs
+
+Use ``malloc leak`` (without arguments) to check the count without
+releasing the snapshot, so you can continue testing::
+
+    => malloc leak start
+    Heap snapshot: 974 allocs
+    => <some operation>
+    => malloc leak
+    No leaks
+    => <another operation>
+    => malloc leak
+    3 new allocs
+    => malloc leak end
+    ...
+
 **Practical workflow**
 
 1. Run ``um t --leak-check -V dm`` (or another suite) to find leaky tests
 2. Use the caller backtrace in the ``-L`` output to find the allocation site
-3. If more detail is needed, add ``malloc_dump_to_file()`` calls or enable
-   ``malloc_log_start()`` to trace all allocations during the operation
+3. If more detail is needed, use ``malloc leak start`` / ``malloc leak end``
+   interactively, or enable ``malloc_log_start()`` to trace all allocations
 4. Fix the leak and verify the test passes
 
 **Dumping heap state on exit**
diff --git a/test/cmd/malloc.c b/test/cmd/malloc.c
index 75e8afdec63..95809845dd7 100644
--- a/test/cmd/malloc.c
+++ b/test/cmd/malloc.c
@@ -54,6 +54,58 @@  static int cmd_test_malloc_dump(struct unit_test_state *uts)
 }
 CMD_TEST(cmd_test_malloc_dump, UTF_CONSOLE);
 
+/* Test 'malloc leak' command using the C API directly */
+static int cmd_test_malloc_leak(struct unit_test_state *uts)
+{
+	struct malloc_leak_snap snap = {};
+	ulong chunk_addr;
+	size_t chunk_sz;
+	void *ptr;
+	int ret;
+
+	/* Take a snapshot, then check with no leaks */
+	ut_assertok(malloc_leak_check_start(&snap));
+	ut_assert(snap.count > 0);
+	ut_asserteq(0, malloc_leak_check_count(&snap));
+
+	/* Allocate something and check it is detected */
+	ptr = malloc(0x42);
+	ut_assertnonnull(ptr);
+	ut_asserteq(1, malloc_leak_check_count(&snap));
+
+	/* Verify freeing clears the leak */
+	free(ptr);
+	ut_asserteq(0, malloc_leak_check_count(&snap));
+
+	/* Re-allocate so end has something to print */
+	ptr = malloc(0x42);
+	ut_assertnonnull(ptr);
+
+	/* End should print the leaked allocation and free the snapshot */
+	ret = malloc_leak_check_end(&snap);
+	ut_asserteq(1, ret);
+
+	/*
+	 * Check the output line shows the correct chunk address and chunk
+	 * size. The chunk address is the malloc pointer minus the mcheck
+	 * header. The caller name is only available with mcheck.
+	 */
+	chunk_addr = (ulong)ptr - malloc_mcheck_hdr_size();
+	chunk_sz = malloc_chunk_size(ptr);
+	if (IS_ENABLED(CONFIG_MCHECK_HEAP_PROTECTION))
+		ut_assert_nextlinen("  %lx %zx %s:", chunk_addr, chunk_sz,
+				    __func__);
+	else
+		ut_assert_nextlinen("  %lx %zx ", chunk_addr, chunk_sz);
+	ut_assert_console_end();
+	ut_assertnull(snap.addr);
+
+	free(ptr);
+
+	return 0;
+}
+CMD_TEST(cmd_test_malloc_leak, UTF_CONSOLE);
+
 #if CONFIG_IS_ENABLED(MCHECK_LOG)
 /* Test 'malloc log' command */
 static int cmd_test_malloc_log(struct unit_test_state *uts)