summaryrefslogtreecommitdiff
path: root/erts
diff options
context:
space:
mode:
authorSverker Eriksson <sverker@erlang.org>2020-12-03 11:27:48 +0100
committerSverker Eriksson <sverker@erlang.org>2020-12-18 18:46:22 +0100
commit856dfc2c90896b10711b1bbf99e27cd167e3958d (patch)
tree1225f14a75d1d1852dafbce79c021f3f2b61cdde /erts
parentfbb59d4c0e89b4a07e119a98d62e421c24406f00 (diff)
downloaderlang-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-xerts/emulator/asan/asan_logs_to_html217
-rw-r--r--erts/emulator/asan/suppress7
-rw-r--r--erts/emulator/beam/erl_bif_info.c137
-rw-r--r--erts/etc/unix/cerl.src22
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`