Skip to content

Commit 0784b1e

Browse files
authored
Re-exec TSan with no ASLR if memory layout is incompatible on Linux (#78351)
TSan's shadow mappings only support 30-bits of ASLR entropy on x86 Linux, and it is not practical to support the maximum of 32-bits (due to pointer compression and the overhead of shadow mappings). Instead, this patch changes TSan to re-exec without ASLR if it encounters an incompatible memory layout, as suggested by Dmitry in google/sanitizers#1716. If ASLR is already disabled but the memory layout is still incompatible, it will abort. This patch involves a bit of refactoring, because the old code is: 1. InitializePlatformEarly() 2. InitializeAllocator() 3. InitializePlatform(): CheckAndProtect() but it may already segfault during InitializeAllocator() if the memory layout is incompatible, before we get a chance to check in CheckAndProtect(). This patch adds CheckAndProtect() during InitializePlatformEarly(), before the allocator is initialized. Naturally, it is necessary to ensure that CheckAndProtect() does *not* allow the heap regions to be occupied here, hence we generalize CheckAndProtect() to optionally check the heap regions. We keep the original behavior of CheckAndProtect() in InitializePlatform() as a last line of defense. We need to be careful not to prematurely abort if ASLR is disabled but TSan was going to re-exec for other reasons (e.g., unlimited stack size); we implement this by moving all the re-exec logic into ReExecIfNeeded().
1 parent 5b0e45c commit 0784b1e

File tree

4 files changed

+138
-50
lines changed

4 files changed

+138
-50
lines changed

compiler-rt/lib/tsan/rtl/tsan_platform.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1024,7 +1024,7 @@ inline uptr RestoreAddr(uptr addr) {
10241024

10251025
void InitializePlatform();
10261026
void InitializePlatformEarly();
1027-
void CheckAndProtect();
1027+
bool CheckAndProtect(bool protect, bool ignore_heap, bool print_warnings);
10281028
void InitializeShadowMemoryPlatform();
10291029
void WriteMemoryProfile(char *buf, uptr buf_size, u64 uptime_ns);
10301030
int ExtractResolvFDs(void *state, int *fds, int nfd);

compiler-rt/lib/tsan/rtl/tsan_platform_linux.cpp

Lines changed: 96 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,86 @@ void InitializeShadowMemoryPlatform() {
214214

215215
#endif // #if !SANITIZER_GO
216216

217+
# if !SANITIZER_GO
218+
static void ReExecIfNeeded() {
219+
// Go maps shadow memory lazily and works fine with limited address space.
220+
// Unlimited stack is not a problem as well, because the executable
221+
// is not compiled with -pie.
222+
bool reexec = false;
223+
// TSan doesn't play well with unlimited stack size (as stack
224+
// overlaps with shadow memory). If we detect unlimited stack size,
225+
// we re-exec the program with limited stack size as a best effort.
226+
if (StackSizeIsUnlimited()) {
227+
const uptr kMaxStackSize = 32 * 1024 * 1024;
228+
VReport(1,
229+
"Program is run with unlimited stack size, which wouldn't "
230+
"work with ThreadSanitizer.\n"
231+
"Re-execing with stack size limited to %zd bytes.\n",
232+
kMaxStackSize);
233+
SetStackSizeLimitInBytes(kMaxStackSize);
234+
reexec = true;
235+
}
236+
237+
if (!AddressSpaceIsUnlimited()) {
238+
Report(
239+
"WARNING: Program is run with limited virtual address space,"
240+
" which wouldn't work with ThreadSanitizer.\n");
241+
Report("Re-execing with unlimited virtual address space.\n");
242+
SetAddressSpaceUnlimited();
243+
reexec = true;
244+
}
245+
246+
// ASLR personality check.
247+
int old_personality = personality(0xffffffff);
248+
bool aslr_on =
249+
(old_personality != -1) && ((old_personality & ADDR_NO_RANDOMIZE) == 0);
250+
251+
# if SANITIZER_ANDROID && (defined(__aarch64__) || defined(__x86_64__))
252+
// After patch "arm64: mm: support ARCH_MMAP_RND_BITS." is introduced in
253+
// linux kernel, the random gap between stack and mapped area is increased
254+
// from 128M to 36G on 39-bit aarch64. As it is almost impossible to cover
255+
// this big range, we should disable randomized virtual space on aarch64.
256+
if (aslr_on) {
257+
VReport(1,
258+
"WARNING: Program is run with randomized virtual address "
259+
"space, which wouldn't work with ThreadSanitizer on Android.\n"
260+
"Re-execing with fixed virtual address space.\n");
261+
CHECK_NE(personality(old_personality | ADDR_NO_RANDOMIZE), -1);
262+
reexec = true;
263+
}
264+
# endif
265+
266+
if (reexec) {
267+
// Don't check the address space since we're going to re-exec anyway.
268+
} else if (!CheckAndProtect(false, false, false)) {
269+
if (aslr_on) {
270+
// Disable ASLR if the memory layout was incompatible.
271+
// Alternatively, we could just keep re-execing until we get lucky
272+
// with a compatible randomized layout, but the risk is that if it's
273+
// not an ASLR-related issue, we will be stuck in an infinite loop of
274+
// re-execing (unless we change ReExec to pass a parameter of the
275+
// number of retries allowed.)
276+
VReport(1,
277+
"WARNING: ThreadSanitizer: memory layout is incompatible, "
278+
"possibly due to high-entropy ASLR.\n"
279+
"Re-execing with fixed virtual address space.\n"
280+
"N.B. reducing ASLR entropy is preferable.\n");
281+
CHECK_NE(personality(old_personality | ADDR_NO_RANDOMIZE), -1);
282+
reexec = true;
283+
} else {
284+
VReport(1,
285+
"FATAL: ThreadSanitizer: memory layout is incompatible, "
286+
"even though ASLR is disabled.\n"
287+
"Please file a bug.\n");
288+
Die();
289+
}
290+
}
291+
292+
if (reexec)
293+
ReExec();
294+
}
295+
# endif
296+
217297
void InitializePlatformEarly() {
218298
vmaSize =
219299
(MostSignificantSetBitIndex(GET_CURRENT_FRAME()) + 1);
@@ -284,6 +364,10 @@ void InitializePlatformEarly() {
284364
}
285365
# endif
286366
# endif
367+
368+
# if !SANITIZER_GO
369+
ReExecIfNeeded();
370+
# endif
287371
}
288372

289373
void InitializePlatform() {
@@ -294,52 +378,22 @@ void InitializePlatform() {
294378
// is not compiled with -pie.
295379
#if !SANITIZER_GO
296380
{
297-
bool reexec = false;
298-
// TSan doesn't play well with unlimited stack size (as stack
299-
// overlaps with shadow memory). If we detect unlimited stack size,
300-
// we re-exec the program with limited stack size as a best effort.
301-
if (StackSizeIsUnlimited()) {
302-
const uptr kMaxStackSize = 32 * 1024 * 1024;
303-
VReport(1, "Program is run with unlimited stack size, which wouldn't "
304-
"work with ThreadSanitizer.\n"
305-
"Re-execing with stack size limited to %zd bytes.\n",
306-
kMaxStackSize);
307-
SetStackSizeLimitInBytes(kMaxStackSize);
308-
reexec = true;
309-
}
310-
311-
if (!AddressSpaceIsUnlimited()) {
312-
Report("WARNING: Program is run with limited virtual address space,"
313-
" which wouldn't work with ThreadSanitizer.\n");
314-
Report("Re-execing with unlimited virtual address space.\n");
315-
SetAddressSpaceUnlimited();
316-
reexec = true;
317-
}
318-
#if SANITIZER_ANDROID && (defined(__aarch64__) || defined(__x86_64__))
319-
// After patch "arm64: mm: support ARCH_MMAP_RND_BITS." is introduced in
320-
// linux kernel, the random gap between stack and mapped area is increased
321-
// from 128M to 36G on 39-bit aarch64. As it is almost impossible to cover
322-
// this big range, we should disable randomized virtual space on aarch64.
323-
// ASLR personality check.
324-
int old_personality = personality(0xffffffff);
325-
if (old_personality != -1 && (old_personality & ADDR_NO_RANDOMIZE) == 0) {
326-
VReport(1, "WARNING: Program is run with randomized virtual address "
327-
"space, which wouldn't work with ThreadSanitizer.\n"
328-
"Re-execing with fixed virtual address space.\n");
329-
CHECK_NE(personality(old_personality | ADDR_NO_RANDOMIZE), -1);
330-
reexec = true;
331-
}
332-
333-
#endif
334-
#if SANITIZER_LINUX && (defined(__aarch64__) || defined(__loongarch_lp64))
381+
# if SANITIZER_LINUX && (defined(__aarch64__) || defined(__loongarch_lp64))
335382
// Initialize the xor key used in {sig}{set,long}jump.
336383
InitializeLongjmpXorKey();
337-
#endif
338-
if (reexec)
339-
ReExec();
384+
# endif
385+
}
386+
387+
// Earlier initialization steps already re-exec'ed until we got a compatible
388+
// memory layout, so we don't expect any more issues here.
389+
if (!CheckAndProtect(true, true, true)) {
390+
Printf(
391+
"FATAL: ThreadSanitizer: unexpectedly found incompatible memory "
392+
"layout.\n");
393+
Printf("FATAL: Please file a bug.\n");
394+
Die();
340395
}
341396

342-
CheckAndProtect();
343397
InitTlsSize();
344398
#endif // !SANITIZER_GO
345399
}

compiler-rt/lib/tsan/rtl/tsan_platform_mac.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ static uptr longjmp_xor_key = 0;
239239
void InitializePlatform() {
240240
DisableCoreDumperIfNecessary();
241241
#if !SANITIZER_GO
242-
CheckAndProtect();
242+
if (!CheckAndProtect(true, true, true)) {
243+
Printf("FATAL: ThreadSanitizer: found incompatible memory layout.\n");
244+
Die();
245+
}
243246

244247
InitializeThreadStateStorage();
245248

compiler-rt/lib/tsan/rtl/tsan_platform_posix.cpp

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,51 @@ static void ProtectRange(uptr beg, uptr end) {
9494
}
9595
}
9696

97-
void CheckAndProtect() {
97+
// CheckAndProtect will check if the memory layout is compatible with TSan.
98+
// Optionally (if 'protect' is true), it will set the memory regions between
99+
// app memory to be inaccessible.
100+
// 'ignore_heap' means it will not consider heap memory allocations to be a
101+
// conflict. Set this based on whether we are calling CheckAndProtect before
102+
// or after the allocator has initialized the heap.
103+
bool CheckAndProtect(bool protect, bool ignore_heap, bool print_warnings) {
98104
// Ensure that the binary is indeed compiled with -pie.
99105
MemoryMappingLayout proc_maps(true);
100106
MemoryMappedSegment segment;
101107
while (proc_maps.Next(&segment)) {
102-
if (IsAppMem(segment.start)) continue;
108+
if (segment.start >= HeapMemBeg() && segment.end <= HeapEnd()) {
109+
if (ignore_heap) {
110+
continue;
111+
} else {
112+
return false;
113+
}
114+
}
115+
116+
// Note: IsAppMem includes if it is heap memory, hence we must
117+
// put this check after the heap bounds check.
118+
if (IsAppMem(segment.start) && IsAppMem(segment.end - 1))
119+
continue;
120+
121+
// Guard page after the heap end
103122
if (segment.start >= HeapMemEnd() && segment.start < HeapEnd()) continue;
123+
104124
if (segment.protection == 0) // Zero page or mprotected.
105125
continue;
126+
106127
if (segment.start >= VdsoBeg()) // vdso
107128
break;
108-
Printf("FATAL: ThreadSanitizer: unexpected memory mapping 0x%zx-0x%zx\n",
109-
segment.start, segment.end);
110-
Die();
129+
130+
// Debug output can break tests. Suppress this message in most cases.
131+
if (print_warnings)
132+
Printf(
133+
"WARNING: ThreadSanitizer: unexpected memory mapping 0x%zx-0x%zx\n",
134+
segment.start, segment.end);
135+
136+
return false;
111137
}
112138

139+
if (!protect)
140+
return true;
141+
113142
# if SANITIZER_IOS && !SANITIZER_IOSSIM
114143
ProtectRange(HeapMemEnd(), ShadowBeg());
115144
ProtectRange(ShadowEnd(), MetaShadowBeg());
@@ -135,8 +164,10 @@ void CheckAndProtect() {
135164
// Older s390x kernels may not support 5-level page tables.
136165
TryProtectRange(user_addr_max_l4, user_addr_max_l5);
137166
#endif
167+
168+
return true;
138169
}
139-
#endif
170+
# endif
140171

141172
} // namespace __tsan
142173

0 commit comments

Comments
 (0)