Skip to content

Commit ed24f06

Browse files
thurstondvar-const
authored andcommitted
[asan] Add experimental 'poison_history_size' flag (llvm#133175)
This adds an experimental flag that will keep track of where the manual memory poisoning (`__asan_poison_memory_region`) is called from, and print the stack trace if the poisoned region is accessed. (Absent this flag, ASan will tell you what code accessed a poisoned region, but not which code set the poison.) This implementation performs best-effort record keeping using ring buffers, as suggested by Vitaly. The size of each ring buffer is set by the `poison_history_size` flag.
1 parent 712f8a2 commit ed24f06

File tree

7 files changed

+236
-0
lines changed

7 files changed

+236
-0
lines changed

compiler-rt/lib/asan/asan_errors.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
//===----------------------------------------------------------------------===//
1313

1414
#include "asan_errors.h"
15+
1516
#include "asan_descriptions.h"
1617
#include "asan_mapping.h"
18+
#include "asan_poisoning.h"
1719
#include "asan_report.h"
1820
#include "asan_stack.h"
1921
#include "sanitizer_common/sanitizer_stackdepot.h"
@@ -600,6 +602,44 @@ static void PrintShadowMemoryForAddress(uptr addr) {
600602
Printf("%s", str.data());
601603
}
602604

605+
static void CheckPoisonRecords(uptr addr) {
606+
if (!AddrIsInMem(addr))
607+
return;
608+
609+
u8 *shadow_addr = (u8 *)MemToShadow(addr);
610+
// If we are in the partial right redzone, look at the next shadow byte.
611+
if (*shadow_addr > 0 && *shadow_addr < 128)
612+
shadow_addr++;
613+
u8 shadow_val = *shadow_addr;
614+
615+
if (shadow_val != kAsanUserPoisonedMemoryMagic)
616+
return;
617+
618+
Printf("\n");
619+
620+
if (flags()->poison_history_size <= 0) {
621+
Printf(
622+
"NOTE: the stack trace above identifies the code that *accessed* "
623+
"the poisoned memory.\n");
624+
Printf(
625+
"To identify the code that *poisoned* the memory, try the "
626+
"experimental setting ASAN_OPTIONS=poison_history_size=<size>.\n");
627+
return;
628+
}
629+
630+
PoisonRecord record;
631+
if (FindPoisonRecord(addr, record)) {
632+
StackTrace poison_stack = StackDepotGet(record.stack_id);
633+
if (poison_stack.size > 0) {
634+
Printf("Memory was manually poisoned by thread T%u:\n", record.thread_id);
635+
poison_stack.Print();
636+
}
637+
} else {
638+
Printf("ERROR: no matching poison tracking record found.\n");
639+
Printf("Try a larger value for ASAN_OPTIONS=poison_history_size=<size>.\n");
640+
}
641+
}
642+
603643
void ErrorGeneric::Print() {
604644
Decorator d;
605645
Printf("%s", d.Error());
@@ -623,6 +663,9 @@ void ErrorGeneric::Print() {
623663
PrintContainerOverflowHint();
624664
ReportErrorSummary(bug_descr, &stack);
625665
PrintShadowMemoryForAddress(addr);
666+
667+
// This is an experimental flag, hence we don't make a special handler.
668+
CheckPoisonRecords(addr);
626669
}
627670

628671
} // namespace __asan

compiler-rt/lib/asan/asan_flags.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ ASAN_FLAG(bool, poison_partial, true,
116116
"stack buffers.")
117117
ASAN_FLAG(bool, poison_array_cookie, true,
118118
"Poison (or not) the array cookie after operator new[].")
119+
ASAN_FLAG(int, poison_history_size, 0,
120+
"[EXPERIMENTAL] Number of most recent memory poisoning calls for "
121+
"which the stack traces will be recorded.")
119122

120123
// Turn off alloc/dealloc mismatch checker on Mac and Windows for now.
121124
// https://github.com/google/sanitizers/issues/131

compiler-rt/lib/asan/asan_poisoning.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,58 @@
2020
#include "sanitizer_common/sanitizer_flags.h"
2121
#include "sanitizer_common/sanitizer_interface_internal.h"
2222
#include "sanitizer_common/sanitizer_libc.h"
23+
#include "sanitizer_common/sanitizer_ring_buffer.h"
24+
#include "sanitizer_common/sanitizer_stackdepot.h"
2325

2426
namespace __asan {
2527

28+
using PoisonRecordRingBuffer = RingBuffer<PoisonRecord>;
29+
2630
static atomic_uint8_t can_poison_memory;
2731

32+
static Mutex poison_records_mutex;
33+
static PoisonRecordRingBuffer *poison_records
34+
SANITIZER_GUARDED_BY(poison_records_mutex) = nullptr;
35+
36+
void AddPoisonRecord(const PoisonRecord &new_record) {
37+
if (flags()->poison_history_size <= 0)
38+
return;
39+
40+
GenericScopedLock<Mutex> l(&poison_records_mutex);
41+
42+
if (poison_records == nullptr)
43+
poison_records = PoisonRecordRingBuffer::New(flags()->poison_history_size);
44+
45+
poison_records->push(new_record);
46+
}
47+
48+
bool FindPoisonRecord(uptr addr, PoisonRecord &match) {
49+
if (flags()->poison_history_size <= 0)
50+
return false;
51+
52+
GenericScopedLock<Mutex> l(&poison_records_mutex);
53+
54+
if (poison_records) {
55+
for (unsigned int i = 0; i < poison_records->size(); i++) {
56+
PoisonRecord record = (*poison_records)[i];
57+
if (record.begin <= addr && addr < record.end) {
58+
internal_memcpy(&match, &record, sizeof(record));
59+
return true;
60+
}
61+
}
62+
}
63+
64+
return false;
65+
}
66+
67+
void SANITIZER_ACQUIRE(poison_records_mutex) AcquirePoisonRecords() {
68+
poison_records_mutex.Lock();
69+
}
70+
71+
void SANITIZER_RELEASE(poison_records_mutex) ReleasePoisonRecords() {
72+
poison_records_mutex.Unlock();
73+
}
74+
2875
void SetCanPoisonMemory(bool value) {
2976
atomic_store(&can_poison_memory, value, memory_order_release);
3077
}
@@ -107,6 +154,20 @@ void __asan_poison_memory_region(void const volatile *addr, uptr size) {
107154
uptr end_addr = beg_addr + size;
108155
VPrintf(3, "Trying to poison memory region [%p, %p)\n", (void *)beg_addr,
109156
(void *)end_addr);
157+
158+
if (flags()->poison_history_size > 0) {
159+
GET_STACK_TRACE(/*max_size=*/16, /*fast=*/false);
160+
u32 current_tid = GetCurrentTidOrInvalid();
161+
162+
u32 stack_id = StackDepotPut(stack);
163+
164+
PoisonRecord record{.stack_id = stack_id,
165+
.thread_id = current_tid,
166+
.begin = beg_addr,
167+
.end = end_addr};
168+
AddPoisonRecord(record);
169+
}
170+
110171
ShadowSegmentEndpoint beg(beg_addr);
111172
ShadowSegmentEndpoint end(end_addr);
112173
if (beg.chunk == end.chunk) {
@@ -147,6 +208,11 @@ void __asan_unpoison_memory_region(void const volatile *addr, uptr size) {
147208
uptr end_addr = beg_addr + size;
148209
VPrintf(3, "Trying to unpoison memory region [%p, %p)\n", (void *)beg_addr,
149210
(void *)end_addr);
211+
212+
// Note: we don't need to update the poison tracking here. Since the shadow
213+
// memory will be unpoisoned, the poison tracking ring buffer entries will be
214+
// ignored.
215+
150216
ShadowSegmentEndpoint beg(beg_addr);
151217
ShadowSegmentEndpoint end(end_addr);
152218
if (beg.chunk == end.chunk) {

compiler-rt/lib/asan/asan_poisoning.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@
2222

2323
namespace __asan {
2424

25+
struct PoisonRecord {
26+
u32 stack_id;
27+
u32 thread_id;
28+
uptr begin;
29+
uptr end;
30+
};
31+
32+
void AddPoisonRecord(const PoisonRecord& new_record);
33+
bool FindPoisonRecord(uptr addr, PoisonRecord& match);
34+
35+
void AcquirePoisonRecords();
36+
void ReleasePoisonRecords();
37+
2538
// Enable/disable memory poisoning.
2639
void SetCanPoisonMemory(bool value);
2740
bool CanPoisonMemory();

compiler-rt/lib/asan/asan_posix.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,17 @@ static void BeforeFork() {
157157
// stuff we need.
158158
__lsan::LockThreads();
159159
__lsan::LockAllocator();
160+
161+
AcquirePoisonRecords();
162+
160163
StackDepotLockBeforeFork();
161164
}
162165

163166
static void AfterFork(bool fork_child) {
164167
StackDepotUnlockAfterFork(fork_child);
168+
169+
ReleasePoisonRecords();
170+
165171
// `_lsan` functions defined regardless of `CAN_SANITIZE_LEAKS` and unlock
166172
// the stuff we need.
167173
__lsan::UnlockAllocator();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Check that __asan_poison_memory_region and ASAN_OPTIONS=poison_history_size work for partial granules.
2+
//
3+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 20 2>&1 | FileCheck %s
4+
//
5+
// Partial granule
6+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 2>&1 | FileCheck %s
7+
8+
#include <stdio.h>
9+
#include <stdlib.h>
10+
11+
extern "C" void __asan_poison_memory_region(void *, size_t);
12+
extern "C" void __asan_unpoison_memory_region(void *, size_t);
13+
14+
void honey_ive_poisoned_the_memory(char *x) {
15+
__asan_poison_memory_region(x + 10, 20);
16+
}
17+
18+
void foo(char *x) { honey_ive_poisoned_the_memory(x); }
19+
20+
int main(int argc, char **argv) {
21+
char *x = new char[64];
22+
x[10] = 0;
23+
foo(x);
24+
// Bytes [0, 9]: addressable
25+
// Bytes [10, 31]: poisoned by A
26+
// Bytes [32, 63]: addressable
27+
28+
int res = x[argc * 10]; // BOOOM
29+
// CHECK: ERROR: AddressSanitizer: use-after-poison
30+
// CHECK: main{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-2]]
31+
32+
// CHECK: Memory was manually poisoned by thread T0:
33+
// CHECK: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-18]]
34+
// CHECK: foo{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-16]]
35+
// CHECK: main{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-12]]
36+
37+
delete[] x;
38+
39+
return 0;
40+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Check that __asan_poison_memory_region and ASAN_OPTIONS=poison_history_size work.
2+
//
3+
// Poisoned access with history
4+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-ABC,CHECK-AC,CHECK-A
5+
//
6+
// Not poisoned access
7+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 %run %t 20 2>&1 | FileCheck %s --check-prefixes=CHECK-ABC,CHECK-B,CHECK-BDE
8+
//
9+
// Poisoned access with history (different stack trace)
10+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 30 30 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-ABC,CHECK-AC,CHECK-C
11+
//
12+
// Poisoned access without history
13+
// RUN: %clangxx_asan -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-BDE,CHECK-D
14+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=0 not %run %t 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-BDE,CHECK-D
15+
16+
// Poisoned access with insufficient history
17+
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1 not %run %t 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-BDE,CHECK-E
18+
19+
#include <stdio.h>
20+
#include <stdlib.h>
21+
22+
extern "C" void __asan_poison_memory_region(void *, size_t);
23+
extern "C" void __asan_unpoison_memory_region(void *, size_t);
24+
25+
void honey_ive_poisoned_the_memory(char *x) {
26+
__asan_poison_memory_region(x, 64); // A
27+
__asan_unpoison_memory_region(x + 16, 8); // B
28+
__asan_poison_memory_region(x + 24, 16); // C
29+
}
30+
31+
void foo(char *x) { honey_ive_poisoned_the_memory(x); }
32+
33+
int main(int argc, char **argv) {
34+
char *x = new char[64];
35+
x[10] = 0;
36+
foo(x);
37+
// Bytes [ 0, 15]: poisoned by A
38+
// Bytes [16, 23]: unpoisoned by B
39+
// Bytes [24, 63]: poisoned by C
40+
41+
int res = x[argc * 10]; // BOOOM
42+
// CHECK-ACDE: ERROR: AddressSanitizer: use-after-poison
43+
// CHECK-ACDE: main{{.*}}use-after-poison-history-size.cpp:[[@LINE-2]]
44+
// CHECK-B-NOT: ERROR: AddressSanitizer: use-after-poison
45+
// CHECK-ABC-NOT: try the experimental setting ASAN_OPTIONS=poison_history_size=
46+
// CHECK-D: try the experimental setting ASAN_OPTIONS=poison_history_size=
47+
48+
// CHECK-AC: Memory was manually poisoned by thread T0:
49+
// CHECK-A: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size.cpp:[[@LINE-23]]
50+
// CHECK-C: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size.cpp:[[@LINE-22]]
51+
// CHECK-AC: foo{{.*}}use-after-poison-history-size.cpp:[[@LINE-20]]
52+
// CHECK-AC: main{{.*}}use-after-poison-history-size.cpp:[[@LINE-16]]
53+
// CHECK-BDE-NOT: Memory was manually poisoned by thread T0:
54+
55+
// CHECK-ABC-NOT: Try a larger value for ASAN_OPTIONS=poison_history_size=
56+
// CHECK-D-NOT: Try a larger value for ASAN_OPTIONS=poison_history_size=
57+
// CHECK-E: Try a larger value for ASAN_OPTIONS=poison_history_size=
58+
59+
delete[] x;
60+
61+
printf("End of program reached\n");
62+
// CHECK-B: End of program reached
63+
64+
return 0;
65+
}

0 commit comments

Comments
 (0)