/* * Copyright (C) the libgit2 contributors. All rights reserved. * * This file is part of libgit2, distributed under the GNU GPL v2 with * a Linking Exception. For full terms see the included COPYING file. */ #include "w32_leakcheck.h" #if defined(GIT_WIN32_LEAKCHECK) #include "Windows.h" #include "Dbghelp.h" #include "win32/posix.h" #include "hash.h" #include "runtime.h" /* Stack frames (for stack tracing, below) */ static bool g_win32_stack_initialized = false; static HANDLE g_win32_stack_process = INVALID_HANDLE_VALUE; static git_win32_leakcheck_stack_aux_cb_alloc g_aux_cb_alloc = NULL; static git_win32_leakcheck_stack_aux_cb_lookup g_aux_cb_lookup = NULL; int git_win32_leakcheck_stack_set_aux_cb( git_win32_leakcheck_stack_aux_cb_alloc cb_alloc, git_win32_leakcheck_stack_aux_cb_lookup cb_lookup) { g_aux_cb_alloc = cb_alloc; g_aux_cb_lookup = cb_lookup; return 0; } /** * Load symbol table data. This should be done in the primary * thread at startup (under a lock if there are other threads * active). */ void git_win32_leakcheck_stack_init(void) { if (!g_win32_stack_initialized) { g_win32_stack_process = GetCurrentProcess(); SymSetOptions(SYMOPT_LOAD_LINES); SymInitialize(g_win32_stack_process, NULL, TRUE); g_win32_stack_initialized = true; } } /** * Cleanup symbol table data. This should be done in the * primary thead at shutdown (under a lock if there are other * threads active). */ void git_win32_leakcheck_stack_cleanup(void) { if (g_win32_stack_initialized) { SymCleanup(g_win32_stack_process); g_win32_stack_process = INVALID_HANDLE_VALUE; g_win32_stack_initialized = false; } } int git_win32_leakcheck_stack_capture(git_win32_leakcheck_stack_raw_data *pdata, int skip) { if (!g_win32_stack_initialized) { git_error_set(GIT_ERROR_INVALID, "git_win32_stack not initialized."); return GIT_ERROR; } memset(pdata, 0, sizeof(*pdata)); pdata->nr_frames = RtlCaptureStackBackTrace( skip+1, GIT_WIN32_LEAKCHECK_STACK_MAX_FRAMES, pdata->frames, NULL); /* If an "aux" data provider was registered, ask it to capture * whatever data it needs and give us an "aux_id" to it so that * we can refer to it later when reporting. */ if (g_aux_cb_alloc) (g_aux_cb_alloc)(&pdata->aux_id); return 0; } int git_win32_leakcheck_stack_compare( git_win32_leakcheck_stack_raw_data *d1, git_win32_leakcheck_stack_raw_data *d2) { return memcmp(d1, d2, sizeof(*d1)); } int git_win32_leakcheck_stack_format( char *pbuf, size_t buf_len, const git_win32_leakcheck_stack_raw_data *pdata, const char *prefix, const char *suffix) { #define MY_MAX_FILENAME 255 /* SYMBOL_INFO has char FileName[1] at the end. The docs say to * to malloc it with extra space for your desired max filename. */ struct { SYMBOL_INFO symbol; char extra[MY_MAX_FILENAME + 1]; } s; IMAGEHLP_LINE64 line; size_t buf_used = 0; unsigned int k; char detail[MY_MAX_FILENAME * 2]; /* filename plus space for function name and formatting */ size_t detail_len; if (!g_win32_stack_initialized) { git_error_set(GIT_ERROR_INVALID, "git_win32_stack not initialized."); return GIT_ERROR; } if (!prefix) prefix = "\t"; if (!suffix) suffix = "\n"; memset(pbuf, 0, buf_len); memset(&s, 0, sizeof(s)); s.symbol.MaxNameLen = MY_MAX_FILENAME; s.symbol.SizeOfStruct = sizeof(SYMBOL_INFO); memset(&line, 0, sizeof(line)); line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); for (k=0; k < pdata->nr_frames; k++) { DWORD64 frame_k = (DWORD64)pdata->frames[k]; DWORD dwUnused; if (SymFromAddr(g_win32_stack_process, frame_k, 0, &s.symbol) && SymGetLineFromAddr64(g_win32_stack_process, frame_k, &dwUnused, &line)) { const char *pslash; const char *pfile; pslash = strrchr(line.FileName, '\\'); pfile = ((pslash) ? (pslash+1) : line.FileName); p_snprintf(detail, sizeof(detail), "%s%s:%d> %s%s", prefix, pfile, line.LineNumber, s.symbol.Name, suffix); } else { /* This happens when we cross into another module. * For example, in CLAR tests, this is typically * the CRT startup code. Just print an unknown * frame and continue. */ p_snprintf(detail, sizeof(detail), "%s??%s", prefix, suffix); } detail_len = strlen(detail); if (buf_len < (buf_used + detail_len + 1)) { /* we don't have room for this frame in the buffer, so just stop. */ break; } memcpy(&pbuf[buf_used], detail, detail_len); buf_used += detail_len; } /* "aux_id" 0 is reserved to mean no aux data. This is needed to handle * allocs that occur before the aux callbacks were registered. */ if (pdata->aux_id > 0) { p_snprintf(detail, sizeof(detail), "%saux_id: %d%s", prefix, pdata->aux_id, suffix); detail_len = strlen(detail); if ((buf_used + detail_len + 1) < buf_len) { memcpy(&pbuf[buf_used], detail, detail_len); buf_used += detail_len; } /* If an "aux" data provider is still registered, ask it to append its detailed * data to the end of ours using the "aux_id" it gave us when this de-duped * item was created. */ if (g_aux_cb_lookup) (g_aux_cb_lookup)(pdata->aux_id, &pbuf[buf_used], (buf_len - buf_used - 1)); } return GIT_OK; } int git_win32_leakcheck_stack( char * pbuf, size_t buf_len, int skip, const char *prefix, const char *suffix) { git_win32_leakcheck_stack_raw_data data; int error; if ((error = git_win32_leakcheck_stack_capture(&data, skip)) < 0) return error; if ((error = git_win32_leakcheck_stack_format(pbuf, buf_len, &data, prefix, suffix)) < 0) return error; return 0; } /* Stack tracing */ #define STACKTRACE_UID_LEN (15) /** * The stacktrace of an allocation can be distilled * to a unique id based upon the stackframe pointers * and ignoring any size arguments. We will use these * UIDs as the (char const*) __FILE__ argument we * give to the CRT malloc routines. */ typedef struct { char uid[STACKTRACE_UID_LEN + 1]; } git_win32_leakcheck_stacktrace_uid; /** * All mallocs with the same stacktrace will be de-duped * and aggregated into this row. */ typedef struct { git_win32_leakcheck_stacktrace_uid uid; /* must be first */ git_win32_leakcheck_stack_raw_data raw_data; unsigned int count_allocs; /* times this alloc signature seen since init */ unsigned int count_allocs_at_last_checkpoint; /* times since last mark */ unsigned int transient_count_leaks; /* sum of leaks */ } git_win32_leakcheck_stacktrace_row; static CRITICAL_SECTION g_crtdbg_stacktrace_cs; /** * CRTDBG memory leak tracking takes a "char const * const file_name" * and stores the pointer in the heap data (instead of allocing a copy * for itself). Normally, this is not a problem, since we usually pass * in __FILE__. But I'm going to lie to it and pass in the address of * the UID in place of the file_name. Also, I do not want to alloc the * stacktrace data (because we are called from inside our alloc routines). * Therefore, I'm creating a very large static pool array to store row * data. This also eliminates the temptation to realloc it (and move the * UID pointers). * * And to efficiently look for duplicates we need an index on the rows * so we can bsearch it. Again, without mallocing. * * If we observe more than MY_ROW_LIMIT unique malloc signatures, we * fall through and use the traditional __FILE__ processing and don't * try to de-dup them. If your testing hits this limit, just increase * it and try again. */ #define MY_ROW_LIMIT (2 * 1024 * 1024) static git_win32_leakcheck_stacktrace_row g_cs_rows[MY_ROW_LIMIT]; static git_win32_leakcheck_stacktrace_row *g_cs_index[MY_ROW_LIMIT]; static unsigned int g_cs_end = MY_ROW_LIMIT; static unsigned int g_cs_ins = 0; /* insertion point == unique allocs seen */ static unsigned int g_count_total_allocs = 0; /* number of allocs seen */ static unsigned int g_transient_count_total_leaks = 0; /* number of total leaks */ static unsigned int g_transient_count_dedup_leaks = 0; /* number of unique leaks */ static bool g_limit_reached = false; /* had allocs after we filled row table */ static unsigned int g_checkpoint_id = 0; /* to better label leak checkpoints */ static bool g_transient_leaks_since_mark = false; /* payload for hook */ /** * Compare function for bsearch on g_cs_index table. */ static int row_cmp(const void *v1, const void *v2) { git_win32_leakcheck_stack_raw_data *d1 = (git_win32_leakcheck_stack_raw_data*)v1; git_win32_leakcheck_stacktrace_row *r2 = (git_win32_leakcheck_stacktrace_row *)v2; return (git_win32_leakcheck_stack_compare(d1, &r2->raw_data)); } /** * Unique insert the new data into the row and index tables. * We have to sort by the stackframe data itself, not the uid. */ static git_win32_leakcheck_stacktrace_row * insert_unique( const git_win32_leakcheck_stack_raw_data *pdata) { size_t pos; if (git__bsearch(g_cs_index, g_cs_ins, pdata, row_cmp, &pos) < 0) { /* Append new unique item to row table. */ memcpy(&g_cs_rows[g_cs_ins].raw_data, pdata, sizeof(*pdata)); sprintf(g_cs_rows[g_cs_ins].uid.uid, "##%08lx", g_cs_ins); /* Insert pointer to it into the proper place in the index table. */ if (pos < g_cs_ins) memmove(&g_cs_index[pos+1], &g_cs_index[pos], (g_cs_ins - pos)*sizeof(g_cs_index[0])); g_cs_index[pos] = &g_cs_rows[g_cs_ins]; g_cs_ins++; } g_cs_index[pos]->count_allocs++; return g_cs_index[pos]; } /** * Hook function to receive leak data from the CRT. (This includes * both ":()" data, but also each of the * various headers and fields. * * Scan this for the special "##" UID forms that we substituted * for the "". Map back to the row data and * increment its leak count. * * See https://msdn.microsoft.com/en-us/library/74kabxyx.aspx * * We suppress the actual crtdbg output. */ static int __cdecl report_hook(int nRptType, char *szMsg, int *retVal) { static int hook_result = TRUE; /* FALSE to get stock dump; TRUE to suppress. */ unsigned int pos; *retVal = 0; /* do not invoke debugger */ if ((szMsg[0] != '#') || (szMsg[1] != '#')) return hook_result; if (sscanf(&szMsg[2], "%08lx", &pos) < 1) return hook_result; if (pos >= g_cs_ins) return hook_result; if (g_transient_leaks_since_mark) { if (g_cs_rows[pos].count_allocs == g_cs_rows[pos].count_allocs_at_last_checkpoint) return hook_result; } g_cs_rows[pos].transient_count_leaks++; if (g_cs_rows[pos].transient_count_leaks == 1) g_transient_count_dedup_leaks++; g_transient_count_total_leaks++; return hook_result; } /** * Write leak data to all of the various places we need. * We force the caller to sprintf() the message first * because we want to avoid fprintf() because it allocs. */ static void my_output(const char *buf) { fwrite(buf, strlen(buf), 1, stderr); OutputDebugString(buf); } /** * For each row with leaks, dump a stacktrace for it. */ static void dump_summary(const char *label) { unsigned int k; char buf[10 * 1024]; if (g_transient_count_total_leaks == 0) return; fflush(stdout); fflush(stderr); my_output("\n"); if (g_limit_reached) { sprintf(buf, "LEAK SUMMARY: de-dup row table[%d] filled. Increase MY_ROW_LIMIT.\n", MY_ROW_LIMIT); my_output(buf); } if (!label) label = ""; if (g_transient_leaks_since_mark) { sprintf(buf, "LEAK CHECKPOINT %d: leaks %d unique %d: %s\n", g_checkpoint_id, g_transient_count_total_leaks, g_transient_count_dedup_leaks, label); my_output(buf); } else { sprintf(buf, "LEAK SUMMARY: TOTAL leaks %d de-duped %d: %s\n", g_transient_count_total_leaks, g_transient_count_dedup_leaks, label); my_output(buf); } my_output("\n"); for (k = 0; k < g_cs_ins; k++) { if (g_cs_rows[k].transient_count_leaks > 0) { sprintf(buf, "LEAK: %s leaked %d of %d times:\n", g_cs_rows[k].uid.uid, g_cs_rows[k].transient_count_leaks, g_cs_rows[k].count_allocs); my_output(buf); if (git_win32_leakcheck_stack_format( buf, sizeof(buf), &g_cs_rows[k].raw_data, NULL, NULL) >= 0) { my_output(buf); } my_output("\n"); } } fflush(stderr); } /** * Initialize our memory leak tracking and de-dup data structures. * This should ONLY be called by git_libgit2_init(). */ void git_win32_leakcheck_stacktrace_init(void) { InitializeCriticalSection(&g_crtdbg_stacktrace_cs); EnterCriticalSection(&g_crtdbg_stacktrace_cs); _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_FILE); _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR); _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); LeaveCriticalSection(&g_crtdbg_stacktrace_cs); } int git_win32_leakcheck_stacktrace_dump( git_win32_leakcheck_stacktrace_options opt, const char *label) { _CRT_REPORT_HOOK old; unsigned int k; int r = 0; #define IS_BIT_SET(o,b) (((o) & (b)) != 0) bool b_set_mark = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_SET_MARK); bool b_leaks_since_mark = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_SINCE_MARK); bool b_leaks_total = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_TOTAL); bool b_quiet = IS_BIT_SET(opt, GIT_WIN32_LEAKCHECK_STACKTRACE_QUIET); if (b_leaks_since_mark && b_leaks_total) { git_error_set(GIT_ERROR_INVALID, "cannot combine LEAKS_SINCE_MARK and LEAKS_TOTAL."); return GIT_ERROR; } if (!b_set_mark && !b_leaks_since_mark && !b_leaks_total) { git_error_set(GIT_ERROR_INVALID, "nothing to do."); return GIT_ERROR; } EnterCriticalSection(&g_crtdbg_stacktrace_cs); if (b_leaks_since_mark || b_leaks_total) { /* All variables with "transient" in the name are per-dump counters * and reset before each dump. This lets us handle checkpoints. */ g_transient_count_total_leaks = 0; g_transient_count_dedup_leaks = 0; for (k = 0; k < g_cs_ins; k++) { g_cs_rows[k].transient_count_leaks = 0; } } g_transient_leaks_since_mark = b_leaks_since_mark; old = _CrtSetReportHook(report_hook); _CrtDumpMemoryLeaks(); _CrtSetReportHook(old); if (b_leaks_since_mark || b_leaks_total) { r = g_transient_count_dedup_leaks; if (!b_quiet) dump_summary(label); } if (b_set_mark) { for (k = 0; k < g_cs_ins; k++) { g_cs_rows[k].count_allocs_at_last_checkpoint = g_cs_rows[k].count_allocs; } g_checkpoint_id++; } LeaveCriticalSection(&g_crtdbg_stacktrace_cs); return r; } /** * Shutdown our memory leak tracking and dump summary data. * This should ONLY be called by git_libgit2_shutdown(). * * We explicitly call _CrtDumpMemoryLeaks() during here so * that we can compute summary data for the leaks. We print * the stacktrace of each unique leak. * * This cleanup does not happen if the app calls exit() * without calling the libgit2 shutdown code. * * This info we print here is independent of any automatic * reporting during exit() caused by _CRTDBG_LEAK_CHECK_DF. * Set it in your app if you also want traditional reporting. */ void git_win32_leakcheck_stacktrace_cleanup(void) { /* At shutdown/cleanup, dump cumulative leak info * with everything since startup. This might generate * extra noise if the caller has been doing checkpoint * dumps, but it might also eliminate some false * positives for resources previously reported during * checkpoints. */ git_win32_leakcheck_stacktrace_dump( GIT_WIN32_LEAKCHECK_STACKTRACE_LEAKS_TOTAL, "CLEANUP"); DeleteCriticalSection(&g_crtdbg_stacktrace_cs); } const char *git_win32_leakcheck_stacktrace(int skip, const char *file) { git_win32_leakcheck_stack_raw_data new_data; git_win32_leakcheck_stacktrace_row *row; const char * result = file; if (git_win32_leakcheck_stack_capture(&new_data, skip+1) < 0) return result; EnterCriticalSection(&g_crtdbg_stacktrace_cs); if (g_cs_ins < g_cs_end) { row = insert_unique(&new_data); result = row->uid.uid; } else { g_limit_reached = true; } g_count_total_allocs++; LeaveCriticalSection(&g_crtdbg_stacktrace_cs); return result; } static void git_win32_leakcheck_global_shutdown(void) { git_win32_leakcheck_stacktrace_cleanup(); git_win32_leakcheck_stack_cleanup(); } bool git_win32_leakcheck_has_leaks(void) { return (g_transient_count_total_leaks > 0); } int git_win32_leakcheck_global_init(void) { git_win32_leakcheck_stacktrace_init(); git_win32_leakcheck_stack_init(); return git_runtime_shutdown_register(git_win32_leakcheck_global_shutdown); } #else int git_win32_leakcheck_global_init(void) { return 0; } #endif