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(-) 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) -- 2.43.0