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(-)
@@ -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
@@ -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);
@@ -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**
@@ -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)