Skip to content

[asan] Add experimental 'poison_history_size' flag #133175

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4cffa15
[asan] Add experimental 'track_poison' flag
thurstond Mar 26, 2025
1c42b53
Use GET_STACK_TRACE (and only use it if track_poison is enabled)
thurstond Mar 26, 2025
32a8dda
Single poison value
thurstond Mar 27, 2025
f080021
Add mutex
thurstond Mar 27, 2025
7882b79
clang-format
thurstond Mar 27, 2025
5db5a53
Cleanup
thurstond Mar 27, 2025
c17cb52
Simplify
thurstond Mar 27, 2025
ae3d17b
Typo
thurstond Mar 27, 2025
4abbd69
Revert added newline
thurstond Mar 27, 2025
c0bbb3e
clang-format
thurstond Mar 27, 2025
f5ec200
clang-format
thurstond Mar 27, 2025
a93e41c
Poison record API
thurstond Apr 1, 2025
b03df8e
Change flag from track_poison to poison_history_size
thurstond Apr 1, 2025
c94f53c
Lazy initialize poison tracking data structure
thurstond Apr 1, 2025
826d2f2
Fork handler
thurstond Apr 1, 2025
9d07a0c
Use u32
thurstond Apr 1, 2025
140b919
Formatting
thurstond Apr 1, 2025
17e4077
Humorless test case
thurstond Apr 1, 2025
249774b
Fix output and improve test
thurstond Apr 1, 2025
19c3dfe
Add test case of Poisoned access with insufficient history
thurstond Apr 1, 2025
3d2ee1d
Update test case and formatting
thurstond Apr 1, 2025
b6be8ed
clang-format
thurstond Apr 1, 2025
1c4f226
Remove unused function declaration
thurstond Apr 1, 2025
ec2a866
Move #include
thurstond Apr 1, 2025
411a091
Add 'flags()->poison_history_size <= 0' guards to ensure no-op
thurstond Apr 7, 2025
1dd4385
Poison records depends on stack depot so must be locked before
thurstond Apr 7, 2025
b4c624e
Only unlock in child
thurstond Apr 7, 2025
287a7b8
Address Vitaly's feedback
thurstond Apr 10, 2025
c2b1947
Hide diagnostic of stack depot did not have a matching stack
thurstond Apr 10, 2025
f4c7b3f
Check next shadow byte if partial granule. Add test case.
thurstond Apr 10, 2025
00e9bf1
Update comment for partial granule test case
thurstond Apr 10, 2025
0014b7e
clang-format
thurstond Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions compiler-rt/lib/asan/asan_errors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
//===----------------------------------------------------------------------===//

#include "asan_errors.h"

#include "asan_descriptions.h"
#include "asan_mapping.h"
#include "asan_poisoning.h"
#include "asan_report.h"
#include "asan_stack.h"
#include "sanitizer_common/sanitizer_stackdepot.h"
Expand Down Expand Up @@ -600,6 +602,44 @@ static void PrintShadowMemoryForAddress(uptr addr) {
Printf("%s", str.data());
}

static void CheckPoisonRecords(uptr addr) {
if (!AddrIsInMem(addr))
return;

u8 *shadow_addr = (u8 *)MemToShadow(addr);
// If we are in the partial right redzone, look at the next shadow byte.
if (*shadow_addr > 0 && *shadow_addr < 128)
shadow_addr++;
u8 shadow_val = *shadow_addr;

if (shadow_val != kAsanUserPoisonedMemoryMagic)
return;

Printf("\n");

if (flags()->poison_history_size <= 0) {
Printf(
"NOTE: the stack trace above identifies the code that *accessed* "
"the poisoned memory.\n");
Printf(
"To identify the code that *poisoned* the memory, try the "
"experimental setting ASAN_OPTIONS=poison_history_size=<size>.\n");
return;
}

PoisonRecord record;
if (FindPoisonRecord(addr, record)) {
StackTrace poison_stack = StackDepotGet(record.stack_id);
if (poison_stack.size > 0) {
Printf("Memory was manually poisoned by thread T%u:\n", record.thread_id);
poison_stack.Print();
}
} else {
Printf("ERROR: no matching poison tracking record found.\n");
Printf("Try a larger value for ASAN_OPTIONS=poison_history_size=<size>.\n");
}
}

void ErrorGeneric::Print() {
Decorator d;
Printf("%s", d.Error());
Expand All @@ -623,6 +663,9 @@ void ErrorGeneric::Print() {
PrintContainerOverflowHint();
ReportErrorSummary(bug_descr, &stack);
PrintShadowMemoryForAddress(addr);

// This is an experimental flag, hence we don't make a special handler.
CheckPoisonRecords(addr);
}

} // namespace __asan
3 changes: 3 additions & 0 deletions compiler-rt/lib/asan/asan_flags.inc
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ ASAN_FLAG(bool, poison_partial, true,
"stack buffers.")
ASAN_FLAG(bool, poison_array_cookie, true,
"Poison (or not) the array cookie after operator new[].")
ASAN_FLAG(int, poison_history_size, 0,
"[EXPERIMENTAL] Number of most recent memory poisoning calls for "
"which the stack traces will be recorded.")

// Turn off alloc/dealloc mismatch checker on Mac and Windows for now.
// https://github.com/google/sanitizers/issues/131
Expand Down
66 changes: 66 additions & 0 deletions compiler-rt/lib/asan/asan_poisoning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,58 @@
#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_interface_internal.h"
#include "sanitizer_common/sanitizer_libc.h"
#include "sanitizer_common/sanitizer_ring_buffer.h"
#include "sanitizer_common/sanitizer_stackdepot.h"

namespace __asan {

using PoisonRecordRingBuffer = RingBuffer<PoisonRecord>;

static atomic_uint8_t can_poison_memory;

static Mutex poison_records_mutex;
static PoisonRecordRingBuffer *poison_records
SANITIZER_GUARDED_BY(poison_records_mutex) = nullptr;

void AddPoisonRecord(const PoisonRecord &new_record) {
if (flags()->poison_history_size <= 0)
return;

GenericScopedLock<Mutex> l(&poison_records_mutex);

if (poison_records == nullptr)
poison_records = PoisonRecordRingBuffer::New(flags()->poison_history_size);

poison_records->push(new_record);
}

bool FindPoisonRecord(uptr addr, PoisonRecord &match) {
if (flags()->poison_history_size <= 0)
return false;

GenericScopedLock<Mutex> l(&poison_records_mutex);

if (poison_records) {
for (unsigned int i = 0; i < poison_records->size(); i++) {
PoisonRecord record = (*poison_records)[i];
if (record.begin <= addr && addr < record.end) {
internal_memcpy(&match, &record, sizeof(record));
return true;
}
}
}

return false;
}

void SANITIZER_ACQUIRE(poison_records_mutex) AcquirePoisonRecords() {
poison_records_mutex.Lock();
}

void SANITIZER_RELEASE(poison_records_mutex) ReleasePoisonRecords() {
poison_records_mutex.Unlock();
}

void SetCanPoisonMemory(bool value) {
atomic_store(&can_poison_memory, value, memory_order_release);
}
Expand Down Expand Up @@ -107,6 +154,20 @@ void __asan_poison_memory_region(void const volatile *addr, uptr size) {
uptr end_addr = beg_addr + size;
VPrintf(3, "Trying to poison memory region [%p, %p)\n", (void *)beg_addr,
(void *)end_addr);

if (flags()->poison_history_size > 0) {
GET_STACK_TRACE(/*max_size=*/16, /*fast=*/false);
u32 current_tid = GetCurrentTidOrInvalid();

u32 stack_id = StackDepotPut(stack);

PoisonRecord record{.stack_id = stack_id,
.thread_id = current_tid,
.begin = beg_addr,
.end = end_addr};
AddPoisonRecord(record);
}

ShadowSegmentEndpoint beg(beg_addr);
ShadowSegmentEndpoint end(end_addr);
if (beg.chunk == end.chunk) {
Expand Down Expand Up @@ -147,6 +208,11 @@ void __asan_unpoison_memory_region(void const volatile *addr, uptr size) {
uptr end_addr = beg_addr + size;
VPrintf(3, "Trying to unpoison memory region [%p, %p)\n", (void *)beg_addr,
(void *)end_addr);

// Note: we don't need to update the poison tracking here. Since the shadow
// memory will be unpoisoned, the poison tracking ring buffer entries will be
// ignored.

ShadowSegmentEndpoint beg(beg_addr);
ShadowSegmentEndpoint end(end_addr);
if (beg.chunk == end.chunk) {
Expand Down
13 changes: 13 additions & 0 deletions compiler-rt/lib/asan/asan_poisoning.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@

namespace __asan {

struct PoisonRecord {
u32 stack_id;
u32 thread_id;
uptr begin;
uptr end;
};

void AddPoisonRecord(const PoisonRecord& new_record);
bool FindPoisonRecord(uptr addr, PoisonRecord& match);

void AcquirePoisonRecords();
void ReleasePoisonRecords();

// Enable/disable memory poisoning.
void SetCanPoisonMemory(bool value);
bool CanPoisonMemory();
Expand Down
6 changes: 6 additions & 0 deletions compiler-rt/lib/asan/asan_posix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,17 @@ static void BeforeFork() {
// stuff we need.
__lsan::LockThreads();
__lsan::LockAllocator();

AcquirePoisonRecords();

StackDepotLockBeforeFork();
}

static void AfterFork(bool fork_child) {
StackDepotUnlockAfterFork(fork_child);

ReleasePoisonRecords();

// `_lsan` functions defined regardless of `CAN_SANITIZE_LEAKS` and unlock
// the stuff we need.
__lsan::UnlockAllocator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Check that __asan_poison_memory_region and ASAN_OPTIONS=poison_history_size work for partial granules.
//
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 20 2>&1 | FileCheck %s
//
// Partial granule
// RUN: %clangxx_asan -O0 %s -o %t && env ASAN_OPTIONS=poison_history_size=1000 not %run %t 2>&1 | FileCheck %s

#include <stdio.h>
#include <stdlib.h>

extern "C" void __asan_poison_memory_region(void *, size_t);
extern "C" void __asan_unpoison_memory_region(void *, size_t);

void honey_ive_poisoned_the_memory(char *x) {
__asan_poison_memory_region(x + 10, 20);
}

void foo(char *x) { honey_ive_poisoned_the_memory(x); }

int main(int argc, char **argv) {
char *x = new char[64];
x[10] = 0;
foo(x);
// Bytes [0, 9]: addressable
// Bytes [10, 31]: poisoned by A
// Bytes [32, 63]: addressable

int res = x[argc * 10]; // BOOOM
// CHECK: ERROR: AddressSanitizer: use-after-poison
// CHECK: main{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-2]]

// CHECK: Memory was manually poisoned by thread T0:
// CHECK: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-18]]
// CHECK: foo{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-16]]
// CHECK: main{{.*}}use-after-poison-history-size-partial-granule.cpp:[[@LINE-12]]

delete[] x;

return 0;
}
65 changes: 65 additions & 0 deletions compiler-rt/test/asan/TestCases/use-after-poison-history-size.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Check that __asan_poison_memory_region and ASAN_OPTIONS=poison_history_size work.
//
// Poisoned access with history
// 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
//
// Not poisoned access
// 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
//
// Poisoned access with history (different stack trace)
// 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
//
// Poisoned access without history
// RUN: %clangxx_asan -O0 %s -o %t && not %run %t 2>&1 | FileCheck %s --check-prefixes=CHECK-ACDE,CHECK-BDE,CHECK-D
// 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

// Poisoned access with insufficient history
// 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

#include <stdio.h>
#include <stdlib.h>

extern "C" void __asan_poison_memory_region(void *, size_t);
extern "C" void __asan_unpoison_memory_region(void *, size_t);

void honey_ive_poisoned_the_memory(char *x) {
__asan_poison_memory_region(x, 64); // A
__asan_unpoison_memory_region(x + 16, 8); // B
__asan_poison_memory_region(x + 24, 16); // C
}

void foo(char *x) { honey_ive_poisoned_the_memory(x); }

int main(int argc, char **argv) {
char *x = new char[64];
x[10] = 0;
foo(x);
// Bytes [ 0, 15]: poisoned by A
// Bytes [16, 23]: unpoisoned by B
// Bytes [24, 63]: poisoned by C

int res = x[argc * 10]; // BOOOM
// CHECK-ACDE: ERROR: AddressSanitizer: use-after-poison
// CHECK-ACDE: main{{.*}}use-after-poison-history-size.cpp:[[@LINE-2]]
// CHECK-B-NOT: ERROR: AddressSanitizer: use-after-poison
// CHECK-ABC-NOT: try the experimental setting ASAN_OPTIONS=poison_history_size=
// CHECK-D: try the experimental setting ASAN_OPTIONS=poison_history_size=

// CHECK-AC: Memory was manually poisoned by thread T0:
// CHECK-A: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size.cpp:[[@LINE-23]]
// CHECK-C: honey_ive_poisoned_the_memory{{.*}}use-after-poison-history-size.cpp:[[@LINE-22]]
// CHECK-AC: foo{{.*}}use-after-poison-history-size.cpp:[[@LINE-20]]
// CHECK-AC: main{{.*}}use-after-poison-history-size.cpp:[[@LINE-16]]
// CHECK-BDE-NOT: Memory was manually poisoned by thread T0:

// CHECK-ABC-NOT: Try a larger value for ASAN_OPTIONS=poison_history_size=
// CHECK-D-NOT: Try a larger value for ASAN_OPTIONS=poison_history_size=
// CHECK-E: Try a larger value for ASAN_OPTIONS=poison_history_size=

delete[] x;

printf("End of program reached\n");
// CHECK-B: End of program reached

return 0;
}