diff options
author | Sverker Eriksson <sverker@erlang.org> | 2020-12-03 11:27:48 +0100 |
---|---|---|
committer | Sverker Eriksson <sverker@erlang.org> | 2020-12-18 18:46:22 +0100 |
commit | 856dfc2c90896b10711b1bbf99e27cd167e3958d (patch) | |
tree | 1225f14a75d1d1852dafbce79c021f3f2b61cdde /erts | |
parent | fbb59d4c0e89b4a07e119a98d62e421c24406f00 (diff) | |
download | erlang-856dfc2c90896b10711b1bbf99e27cd167e3958d.tar.gz |
Produce nice asan logs
Support in cerl, test_server, erlang:system_info({memory_checker,_})
and a new escript asan_logs_to_html.
Diffstat (limited to 'erts')
-rwxr-xr-x | erts/emulator/asan/asan_logs_to_html | 217 | ||||
-rw-r--r-- | erts/emulator/asan/suppress | 7 | ||||
-rw-r--r-- | erts/emulator/beam/erl_bif_info.c | 137 | ||||
-rw-r--r-- | erts/etc/unix/cerl.src | 22 |
4 files changed, 332 insertions, 51 deletions
diff --git a/erts/emulator/asan/asan_logs_to_html b/erts/emulator/asan/asan_logs_to_html new file mode 100755 index 0000000000..c1f787f380 --- /dev/null +++ b/erts/emulator/asan/asan_logs_to_html @@ -0,0 +1,217 @@ +#!/usr/bin/env escript +%% -*- erlang -*- + +%% Parse address sanitizer log files generated from test runs with +%% with environment variables ASAN_LOG_DIR and TS_RUN_EMU=asan set. + +%% Repeated leak reports are ignored and additional leaks of same type +%% as seen before are identified as such. + +-mode(compile). + +main([]) -> + help(); +main(["--help"]) -> + help(); +main([OutDir]) -> + case os:getenv("ASAN_LOG_DIR") of + false -> + io:format(standard_error, + "\nMissing asan log directory argument and environment\n" + "variable ASAN_LOG_DIR is not set.\n\n",[]), + help(); + InDir -> + run(OutDir, InDir) + end; +main([OutDir, InDir]) -> + run(OutDir, InDir). + + +help() -> + io:format("\nSyntax: asan_log_to_html OutDir [InDir]\n" + "\nParses all address-sanetizer log files in InDir\n" + "and generates a summary file OutDir/asan_summary.html.\n" + "Environment variable ASAN_LOG_DIR is used if InDir\n" + "is not specified\n\n", []). + +run(OutDir, InDir) -> + {ok, InFilesUS} = file:list_dir(InDir), + InFiles = lists:sort(InFilesUS), + + OutFile = filename:join(OutDir, "asan_summary.html"), + {ok, FD} = file:open(OutFile, [write]), + + ok = file:write(FD, <<"<!DOCTYPE html>\n" + "<html>\n" + "<head><title>Address Sanitizer</title></head>\n" + "<body>\n" + "<h1>Address Sanitizer</h1>\n">>), + + lists:foldl(fun(File, Acc) -> + io:format("analyze ~s\n", [File]), + analyze_log_file(filename:join(InDir,File), + FD, Acc) + end, + {#{}, none, none}, + InFiles), + + ok = io:format(FD, "<hr>\n", []), + + Time = calendar:system_time_to_rfc3339(erlang:system_time(second), + [{time_designator, 32}]), + %%{_, _, ThisFile} = code:get_object_code(?MODULE), + ThisFile = escript:script_name(), + User = string:trim(os:cmd("whoami")), + {ok, Host} = inet:gethostname(), + ok = io:format(FD, "<p><small>This page was generated ~s\n" + " by <tt>~s</tt>\n" + " run by ~s@~s.</small></p>\n", + [Time, ThisFile, User, Host]), + + ok = file:write(FD, <<"</body>\n</html>\n">>), + ok = file:close(FD), + io:format("Generated file ~s\n", [OutFile]), + ok. + +analyze_log_file(SrcFile, OutFD, {LeakMap0, PrevApp, RegEx0}) -> + + [_Exe, App | _] = string:lexemes(filename:basename(SrcFile), "-"), + case App of + PrevApp -> ignore; + _ -> + Line = case PrevApp of + none -> ""; + _ -> "<hr>" + end, + ok = io:format(OutFD, "~s<h2>~s</h2>\n", [Line, App]) + end, + + {ok, Bin} = file:read_file(SrcFile), + + {Leaks, RegEx1} = + run_regex(Bin, RegEx0, + %% LeakReport + "(?:(Direct|Indirect) leak of ([0-9]+) byte\\(s\\) " + "in ([0-9]+) object\\(s\\) allocated from:\n" + "((?:[ \t]*#[0-9]+.+\n)+))" % Call stack + "|" + %% ErrorReport + "(?:(==ERROR: AddressSanitizer:.*\n" + "(?:.*\n)+?)" % any lines (non-greedy) + "^(?:==|--))" % stop at line begining with == or -- + "|" + %% Skipped + "(?:^[=-]+$)", % skip lines consisting only of = or - + + [multiline], + [global, {capture, all, binary}]), + + %% We indentify a leak by its type (direct or indirect) + %% and its full call stack. + + LeakChecker = + fun([ErrorReport, <<>>, <<>>, <<>>, <<>>, Captured], + {Out, MatchCnt, LM0}) -> + FD = fd(Out), + ok = io:format(FD, "<p><pre~s>\n", [style(error)]), + ok = file:write(FD, Captured), + ok = io:format(FD, "</pre></p>\n", []), + {FD, MatchCnt + byte_size(ErrorReport), LM0}; + + ([LeakReport, Type, BytesBin, BlocksBin, Stack | _], + {Out, MatchCnt0, LM0}) -> + Bytes = binary_to_integer(BytesBin), + Blocks = binary_to_integer(BlocksBin), + MatchCnt1 = MatchCnt0 + byte_size(LeakReport), + Key = {Type, Stack}, + case lookup_leak(LM0, Key) of + undefined -> + %% A new leak + LM1 = insert_leak(LM0, Key, Bytes, Blocks), + FD = fd(Out), + ok = io:format(FD, "<p><pre~s>\n", [style(new, Type)]), + ok = file:write(FD, LeakReport), + ok = io:format(FD, "</pre></p>\n", []), + {FD, MatchCnt1, LM1}; + + {Bytes, Blocks} -> + %% Exact same leak(s) repeated, ignore + {Out, MatchCnt1, LM0}; + + {OldBytes, OldBlocks} -> + %% More leaked bytes/blocks of same type&stack as before + LM1 = insert_leak(LM0, Key, Bytes, Blocks), + FD = fd(Out), + ok = io:format(FD, "<p><pre~s>\n", [style(more, Type)]), + ok = io:format(FD, "More ~s leak of ~w(~w) byte(s) " + "in ~w(~w) object(s) allocated from:\n", + [Type, Bytes - OldBytes, Bytes, + Blocks - OldBlocks, Blocks]), + ok = file:write(FD, Stack), + ok = io:format(FD, "</pre></p>\n", []), + {FD, MatchCnt1, LM1} + end; + ([SkipLine], {Out, MatchCnt0, LM0}) -> + nomatch = binary:match(SkipLine, <<"\n">>), % Assert single line + {Out, MatchCnt0 + byte_size(SkipLine), LM0} + end, + Out0 = {OutFD, SrcFile}, + {Out1, MatchedBytes, LeakMap1} = lists:foldl(LeakChecker, + {Out0, 0, LeakMap0}, + Leaks), + + case MatchedBytes < byte_size(Bin)-500 of + true -> + FD = fd(Out1), + ok = io:format(FD, "<h2>WARNING!!! May be unmatched error reports" + " in file ~s:</h2>\n", [SrcFile]), + FD; + false -> + Out1 + end, + {LeakMap1, App, RegEx1}. + +lookup_leak(LeakMap, Key) -> + maps:get(Key, LeakMap, undefined). + +insert_leak(LeakMap, Key, Bytes, Blocks) -> + LeakMap#{Key => {Bytes, Blocks}}. + +fd({FD, SrcFile}) -> + TcFile = filename:basename(SrcFile), + case string:lexemes(TcFile, "-") of + [_Exe, App, _Rest] -> + ok = io:format(FD, "<h3>Before first test case of ~s</h3>\n", + [App]); + [_Exe, _App, "tc", Num, Mod, Rest] -> + [Func | _] = string:lexemes(Rest, "."), + ok = io:format(FD, "<h3>Test case #~s ~s:~s</h3>\n", [Num, Mod, Func]); + _ -> + ok = io:format(FD, "<h3>Strange log file name '~s'</h3>\n", + [SrcFile]) + end, + FD; +fd(FD) -> + FD. + +style(error) -> + " style=\"background-color:Tomato;\"". + +style(new, <<"Direct">>) -> + " style=\"background-color:orange;\""; +style(new, <<"Indirect">>) -> + ""; +style(more, _) -> + " style=\"background-color:yellow;\"". + + +run_regex(Bin, none, RegExString, CompileOpts, RunOpts) -> + {ok, RegEx} = re:compile(RegExString, CompileOpts), + run_regex(Bin, RegEx, none, none, RunOpts); +run_regex(Bin, RegEx, _, _, RunOpts) -> + case re:run(Bin, RegEx, RunOpts) of + nomatch -> + {[], RegEx}; + {match, List} -> + {List, RegEx} + end. diff --git a/erts/emulator/asan/suppress b/erts/emulator/asan/suppress new file mode 100644 index 0000000000..e7ac3632a6 --- /dev/null +++ b/erts/emulator/asan/suppress @@ -0,0 +1,7 @@ +leak:erts_alloc_permanent_cache_aligned + +# Harmless leak of ErtsThrPrgrData from async threads in exiting emulator +leak:erts_thr_progress_register_unmanaged_thread + +# Block passed to sigaltstack() +leak:sys_thread_init_signal_stack diff --git a/erts/emulator/beam/erl_bif_info.c b/erts/emulator/beam/erl_bif_info.c index cb32c5c255..d4d1264916 100644 --- a/erts/emulator/beam/erl_bif_info.c +++ b/erts/emulator/beam/erl_bif_info.c @@ -62,8 +62,11 @@ #endif #ifdef VALGRIND -#include <valgrind/valgrind.h> -#include <valgrind/memcheck.h> +# include <valgrind/valgrind.h> +# include <valgrind/memcheck.h> +#endif +#ifdef ADDRESS_SANITIZER +# include <sanitizer/lsan_interface.h> #endif static Export* alloc_info_trap = NULL; @@ -2123,6 +2126,28 @@ current_stacktrace(Process *p, ErtsHeapFactory *hfact, Process* rp, return res; } +#if defined(VALGRIND) || defined(ADDRESS_SANITIZER) +static int iolist_to_tmp_buf(Eterm iolist, char** bufp) +{ + ErlDrvSizeT buf_size = 1024; /* Try with 1KB first */ + char *buf = erts_alloc(ERTS_ALC_T_TMP, buf_size); + ErlDrvSizeT r = erts_iolist_to_buf(iolist, (char*) buf, buf_size - 1); + if (ERTS_IOLIST_TO_BUF_FAILED(r)) { + erts_free(ERTS_ALC_T_TMP, (void *) buf); + if (erts_iolist_size(iolist, &buf_size)) { + return 0; + } + buf_size++; + buf = erts_alloc(ERTS_ALC_T_TMP, buf_size); + r = erts_iolist_to_buf(iolist, (char*) buf, buf_size - 1); + ASSERT(r == buf_size - 1); + } + buf[buf_size - 1 - r] = '\0'; + *bufp = buf; + return 1; +} +#endif + /* * This function takes care of calls to erlang:system_info/1 when the argument * is a tuple. @@ -2185,59 +2210,45 @@ info_1_tuple(Process* BIF_P, /* Pointer to current process. */ goto badarg; ERTS_BIF_PREP_TRAP1(ret, erts_format_cpu_topology_trap, BIF_P, res); return ret; -#if defined(PURIFY) || defined(VALGRIND) - } else if (ERTS_IS_ATOM_STR("error_checker", sel) -#if defined(PURIFY) - || sel == am_purify -#elif defined(VALGRIND) - || ERTS_IS_ATOM_STR("valgrind", sel) + } else if (ERTS_IS_ATOM_STR("memory_checker", sel)) { + if (arity == 2 && ERTS_IS_ATOM_STR("test_leak", *tp)) { +#if defined(VALGRIND) || defined(ADDRESS_SANITIZER) + erts_alloc(ERTS_ALC_T_HEAP , 100); #endif - ) { - if (*tp == am_memory) { -#if defined(PURIFY) - BIF_RET(erts_make_integer(purify_new_leaks(), BIF_P)); -#elif defined(VALGRIND) -# ifdef VALGRIND_DO_ADDED_LEAK_CHECK + BIF_RET(am_ok); + } + else if (arity == 2 && ERTS_IS_ATOM_STR("test_overflow", *tp)) { + static int test[2]; + BIF_RET(make_small(test[2])); + } +#if defined(VALGRIND) || defined(ADDRESS_SANITIZER) + if (arity == 2 && *tp == am_running) { +# if defined(VALGRIND) + if (RUNNING_ON_VALGRIND) + BIF_RET(ERTS_MAKE_AM("valgrind")); +# elif defined(ADDRESS_SANITIZER) + BIF_RET(ERTS_MAKE_AM("asan")); +# endif + } + else if (arity == 2 && ERTS_IS_ATOM_STR("check_leaks", *tp)) { +# if defined(VALGRIND) +# ifdef VALGRIND_DO_ADDED_LEAK_CHECK VALGRIND_DO_ADDED_LEAK_CHECK; -# else +# else VALGRIND_DO_LEAK_CHECK; +# endif + BIF_RET(am_ok); +# elif defined(ADDRESS_SANITIZER) + __lsan_do_recoverable_leak_check(); + BIF_RET(am_ok); # endif - BIF_RET(make_small(0)); -#endif - } else if (*tp == am_fd) { -#if defined(PURIFY) - BIF_RET(erts_make_integer(purify_new_fds_inuse(), BIF_P)); -#elif defined(VALGRIND) - /* Not present in valgrind... */ - BIF_RET(make_small(0)); -#endif - } else if (*tp == am_running) { -#if defined(PURIFY) - BIF_RET(purify_is_running() ? am_true : am_false); -#elif defined(VALGRIND) - BIF_RET(RUNNING_ON_VALGRIND ? am_true : am_false); -#endif - } else if (is_list(*tp)) { -#if defined(PURIFY) -# define ERTS_ERROR_CHECKER_PRINTF purify_printf -#elif defined(VALGRIND) -# define ERTS_ERROR_CHECKER_PRINTF VALGRIND_PRINTF -#endif - ErlDrvSizeT buf_size = 8*1024; /* Try with 8KB first */ - char *buf = erts_alloc(ERTS_ALC_T_TMP, buf_size); - ErlDrvSizeT r = erts_iolist_to_buf(*tp, (char*) buf, buf_size - 1); - if (ERTS_IOLIST_TO_BUF_FAILED(r)) { - erts_free(ERTS_ALC_T_TMP, (void *) buf); - if (erts_iolist_size(*tp, &buf_size)) { - goto badarg; - } - buf_size++; - buf = erts_alloc(ERTS_ALC_T_TMP, buf_size); - r = erts_iolist_to_buf(*tp, (char*) buf, buf_size - 1); - ASSERT(r == buf_size - 1); - } - buf[buf_size - 1 - r] = '\0'; - ERTS_ERROR_CHECKER_PRINTF("%s\n", buf); + } +# if defined(VALGRIND) + if (arity == 3 && tp[0] == am_print && is_list(tp[1])) { + char* buf; + if (!iolist_to_tmp_buf(tp[1], &buf)) + goto badarg; + VALGRIND_PRINTF("%s\n", buf); erts_free(ERTS_ALC_T_TMP, (void *) buf); BIF_RET(am_true); #undef ERTS_ERROR_CHECKER_PRINTF @@ -2257,6 +2268,30 @@ info_1_tuple(Process* BIF_P, /* Pointer to current process. */ } else if (*tp == am_running) { BIF_RET(quantify_is_running() ? am_true : am_false); } +# endif +# if defined(ADDRESS_SANITIZER) + if (arity == 3 && ERTS_IS_ATOM_STR("log",tp[0]) && is_list(tp[1])) { + static char *active_log = NULL; + static int active_log_len; + Eterm ret = NIL; + char* buf; + if (!iolist_to_tmp_buf(tp[1], &buf)) + goto badarg; + erts_rwmtx_rwlock(&erts_dist_table_rwmtx); /* random lock abuse */ + __sanitizer_set_report_path(buf); + if (active_log) { + Eterm *hp = HAlloc(BIF_P, 2 * active_log_len); + ret = erts_bld_string_n(&hp, 0, active_log, active_log_len); + erts_free(ERTS_ALC_T_DEBUG, active_log); + } + active_log_len = sys_strlen(buf); + active_log = erts_alloc(ERTS_ALC_T_DEBUG, active_log_len + 1); + sys_memcpy(active_log, buf, active_log_len + 1); + erts_rwmtx_rwunlock(&erts_dist_table_rwmtx); + erts_free(ERTS_ALC_T_TMP, (void *) buf); + BIF_RET(ret); + } +# endif #endif #if defined(__GNUC__) && defined(HAVE_SOLARIS_SPARC_PERFMON) } else if (ERTS_IS_ATOM_STR("ultrasparc_set_pcr", sel)) { diff --git a/erts/etc/unix/cerl.src b/erts/etc/unix/cerl.src index ea9f8a7978..3889308a67 100644 --- a/erts/etc/unix/cerl.src +++ b/erts/etc/unix/cerl.src @@ -278,6 +278,28 @@ if [ $skip_erlexec = yes ]; then set -- $beam_args IFS="$SAVE_IFS" fi +if [ $run_asan = yes ]; then + # Leak sanitizer options + if [ "x${LSAN_OPTIONS#*suppressions=}" = "x$LSAN_OPTIONS" ]; then + export LSAN_OPTIONS + if [ "x$ERL_TOP" != "x" ]; then + LSAN_OPTIONS="$LSAN_OPTIONS:suppressions=$ERL_TOP/erts/emulator/asan/suppress" + else + echo "No leak-sanitizer suppression file found in \$LSAN_OPTIONS" + echo "and \$ERL_TOP not set." + fi + fi + # Address sanitizer options + export ASAN_OPTIONS + if [ "x$ASAN_LOG_DIR" != "x" ]; then + if [ "x${ASAN_OPTIONS#*log_path=}" = "x$ASAN_OPTIONS" ]; then + ASAN_OPTIONS="$ASAN_OPTIONS:log_path=$ASAN_LOG_DIR/$EMU_NAME-$ASAN_LOGFILE_PREFIX-0" + fi + fi + if [ "x${ASAN_OPTIONS#*halt_on_error=}" = "x$ASAN_OPTIONS" ]; then + ASAN_OPTIONS="$ASAN_OPTIONS:halt_on_error=false" + fi +fi if [ "x$GDB" = "x" ]; then if [ $run_valgrind = yes ]; then valversion=`valgrind --version` |