Skip to content

Commit c584c42

Browse files
[asan] Speed up ASan ODR indicator-based checking (#100923)
**Summary**: When ASan checks for a potential ODR violation on a global it loops over a linked list of all globals to find those with the matching value of an indicator. With the default setting 'detect_odr_violation=1', ASan doesn't report violations on same-size globals but it still has to traverse the list. For larger binaries with a ton of shared libs and globals (and a non-trivial volume of same-sized duplicates) this gets extremely expensive. This patch adds an indicator indexed (multi-)map of globals to speed up the search. > Note: asan used to use a map to store globals a while ago which was replaced with a list when the codebase [moved off of STL](e4bada2). Internally we see many examples where ODR checking takes *seconds* (even double digits). With this patch it's practically free and `__asan_register_globals` doesn't show up prominently in the perf profile anymore. There are several high-level questions: 1. I understand that the intent is that we hit the slow path rarely, ideally once before the process dies with an error. But in practice we hit the slow path a lot. It feels reasonable to keep the amount of work bounded even in the worst case, even if it requires a bit of extra memory. But if not, it'd be great to learn about the tradeoffs. 2. Poisoning based ODR checking remains on the slow path. Internally we build everything with `-fsanitize-address-use-odr-indicator` so I'm not sure if poisoning-based check would exhibit the same behavior (looking at the code, the shape looks very similar, so it might?). 3. Globals with an ODR indicator of `-1` need to be skipped for the purposes of ODR checking (cf. a257639). But they are still getting added to the list of globals and hence take up space and slow down the iteration over the list of globals. It would be a good saving if we could avoid adding them to the globals list. 4. Any reason to use a linked list instead of e.g. a vector to store globals? **Test Plan**: * `cmake --build build --target check-asan` looks good * Perf-wise things look good when linking against this version of compiler-rt. --------- Co-authored-by: Vitaly Buka <[email protected]>
1 parent 8f33f1d commit c584c42

File tree

2 files changed

+95
-12
lines changed

2 files changed

+95
-12
lines changed

compiler-rt/lib/asan/asan_globals.cpp

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "asan_suppressions.h"
2222
#include "asan_thread.h"
2323
#include "sanitizer_common/sanitizer_common.h"
24+
#include "sanitizer_common/sanitizer_dense_map.h"
2425
#include "sanitizer_common/sanitizer_list.h"
2526
#include "sanitizer_common/sanitizer_mutex.h"
2627
#include "sanitizer_common/sanitizer_placement_new.h"
@@ -36,9 +37,11 @@ struct GlobalListNode {
3637
GlobalListNode *next = nullptr;
3738
};
3839
typedef IntrusiveList<GlobalListNode> ListOfGlobals;
40+
typedef DenseMap<uptr, ListOfGlobals> MapOfGlobals;
3941

4042
static Mutex mu_for_globals;
4143
static ListOfGlobals list_of_all_globals;
44+
static MapOfGlobals map_of_globals_by_indicator;
4245

4346
static const int kDynamicInitGlobalsInitialCapacity = 512;
4447
struct DynInitGlobal {
@@ -148,21 +151,25 @@ static void CheckODRViolationViaIndicator(const Global *g) {
148151
// Instrumentation requests to skip ODR check.
149152
if (g->odr_indicator == UINTPTR_MAX)
150153
return;
154+
155+
ListOfGlobals &relevant_globals =
156+
map_of_globals_by_indicator[g->odr_indicator];
157+
151158
u8 *odr_indicator = reinterpret_cast<u8 *>(g->odr_indicator);
152-
if (*odr_indicator == UNREGISTERED) {
153-
*odr_indicator = REGISTERED;
154-
return;
155-
}
156-
// If *odr_indicator is DEFINED, some module have already registered
157-
// externally visible symbol with the same name. This is an ODR violation.
158-
for (const auto &l : list_of_all_globals) {
159-
if (g->odr_indicator == l.g->odr_indicator &&
160-
(flags()->detect_odr_violation >= 2 || g->size != l.g->size) &&
161-
!IsODRViolationSuppressed(g->name)) {
162-
ReportODRViolation(g, FindRegistrationSite(g), l.g,
163-
FindRegistrationSite(l.g));
159+
if (*odr_indicator == REGISTERED) {
160+
// If *odr_indicator is REGISTERED, some module have already registered
161+
// externally visible symbol with the same name. This is an ODR violation.
162+
for (const auto &l : relevant_globals) {
163+
if ((flags()->detect_odr_violation >= 2 || g->size != l.g->size) &&
164+
!IsODRViolationSuppressed(g->name))
165+
ReportODRViolation(g, FindRegistrationSite(g), l.g,
166+
FindRegistrationSite(l.g));
164167
}
168+
} else { // UNREGISTERED
169+
*odr_indicator = REGISTERED;
165170
}
171+
172+
AddGlobalToList(relevant_globals, g);
166173
}
167174

168175
// Check ODR violation for given global G by checking if it's already poisoned.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Test that captures the current behavior of indicator-based ASan ODR checker
2+
// when globals get unregistered.
3+
//
4+
// RUN: %clangxx_asan -g -O0 -DSHARED_LIB -DSIZE=1 %s -fPIC -shared -o %t-so-1.so
5+
// RUN: %clangxx_asan -g -O0 -DSHARED_LIB -DSIZE=2 %s -fPIC -shared -o %t-so-2.so
6+
// RUN: %clangxx_asan -g -O0 %s %libdl -Wl,--export-dynamic -o %t
7+
// RUN: %env_asan_opts=report_globals=2:detect_odr_violation=1 %run %t 2>&1 | FileCheck %s
8+
9+
#include <cstdlib>
10+
#include <dlfcn.h>
11+
#include <stdio.h>
12+
#include <string>
13+
14+
#ifdef SHARED_LIB
15+
namespace foo {
16+
char G[SIZE];
17+
}
18+
#else // SHARED_LIB
19+
void *dlopen_or_die(std::string &path) {
20+
void *handle = dlopen(path.c_str(), RTLD_NOW);
21+
if (handle) {
22+
printf("Successfully called dlopen() on %s\n", path.c_str());
23+
} else {
24+
printf("Error in dlopen(): %s\n", dlerror());
25+
std::exit(1);
26+
}
27+
28+
return handle;
29+
}
30+
31+
void dlclose_or_die(void *handle, std::string &path) {
32+
if (!dlclose(handle)) {
33+
printf("Successfully called dlclose() on %s\n", path.c_str());
34+
} else {
35+
printf("Error in dlclose(): %s\n", dlerror());
36+
std::exit(1);
37+
}
38+
}
39+
40+
namespace foo {
41+
char G[1];
42+
}
43+
44+
// main has its own version of foo::G
45+
// CHECK: Added Global[[MAIN_G:[^\s]+]] size=1/32 name=foo::G {{.*}}
46+
int main(int argc, char *argv[]) {
47+
std::string base_path = std::string(argv[0]);
48+
49+
std::string path1 = base_path + "-so-1.so";
50+
// dlopen() brings another foo::G but it matches MAIN_G in size so it's not a
51+
// violation
52+
//
53+
//
54+
// CHECK: Added Global[[SO1_G:[^\s]+]] size=1/32 name=foo::G {{.*}}
55+
// CHECK-NOT: ERROR: AddressSanitizer: odr-violation
56+
void *handle1 = dlopen_or_die(path1);
57+
// CHECK: Removed Global[[SO1_G]] size=1/32 name=foo::G {{.*}}
58+
dlclose_or_die(handle1, path1);
59+
60+
// At this point the indicator for foo::G is switched to UNREGISTERED for
61+
// **both** MAIN_G and SO1_G because the indicator value is shared.
62+
63+
std::string path2 = base_path + "-so-2.so";
64+
// CHECK: Added Global[[SO2_G:[^\s]+]] size=2/32 name=foo::G {{.*}}
65+
//
66+
// This brings another foo::G but now different in size from MAIN_G. We
67+
// should've reported a violation, but we actually don't because of what's
68+
// described on line60
69+
//
70+
// CHECK-NOT: ERROR: AddressSanitizer: odr-violation
71+
void *handle2 = dlopen_or_die(path2);
72+
// CHECK: Removed Global[[MAIN_G]] size=1/32 name=foo::G {{.*}}
73+
// CHECK: Removed Global[[SO2_G]] size=2/32 name=foo::G {{.*}}
74+
}
75+
76+
#endif // SHARED_LIB

0 commit comments

Comments
 (0)