[compiler-rt] 721af7e - [asan] Add experimental 'poison_history_size' flag (#133175)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Apr 10 13:53:57 PDT 2025
Author: Thurston Dang
Date: 2025-04-10T13:53:54-07:00
New Revision: 721af7e636c3dc9bbc5b13ef52331addc8242b50
URL: https://github.com/llvm/llvm-project/commit/721af7e636c3dc9bbc5b13ef52331addc8242b50
DIFF: https://github.com/llvm/llvm-project/commit/721af7e636c3dc9bbc5b13ef52331addc8242b50.diff
LOG: [asan] Add experimental 'poison_history_size' flag (#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.
Added:
compiler-rt/test/asan/TestCases/use-after-poison-history-size-partial-granule.cpp
compiler-rt/test/asan/TestCases/use-after-poison-history-size.cpp
Modified:
compiler-rt/lib/asan/asan_errors.cpp
compiler-rt/lib/asan/asan_flags.inc
compiler-rt/lib/asan/asan_poisoning.cpp
compiler-rt/lib/asan/asan_poisoning.h
compiler-rt/lib/asan/asan_posix.cpp
Removed:
################################################################################
diff --git a/compiler-rt/lib/asan/asan_errors.cpp b/compiler-rt/lib/asan/asan_errors.cpp
index 4f112cc5d1bca..2a207cd06ccac 100644
--- a/compiler-rt/lib/asan/asan_errors.cpp
+++ b/compiler-rt/lib/asan/asan_errors.cpp
@@ -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"
@@ -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());
@@ -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
diff --git a/compiler-rt/lib/asan/asan_flags.inc b/compiler-rt/lib/asan/asan_flags.inc
index fad1577d912a5..32e6d3405533d 100644
--- a/compiler-rt/lib/asan/asan_flags.inc
+++ b/compiler-rt/lib/asan/asan_flags.inc
@@ -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
diff --git a/compiler-rt/lib/asan/asan_poisoning.cpp b/compiler-rt/lib/asan/asan_poisoning.cpp
index 762670632f4e0..1bee8ef5463e4 100644
--- a/compiler-rt/lib/asan/asan_poisoning.cpp
+++ b/compiler-rt/lib/asan/asan_poisoning.cpp
@@ -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);
}
@@ -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) {
@@ -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) {
diff --git a/compiler-rt/lib/asan/asan_poisoning.h b/compiler-rt/lib/asan/asan_poisoning.h
index b68af1086e17d..4b2d6220b1b14 100644
--- a/compiler-rt/lib/asan/asan_poisoning.h
+++ b/compiler-rt/lib/asan/asan_poisoning.h
@@ -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();
diff --git a/compiler-rt/lib/asan/asan_posix.cpp b/compiler-rt/lib/asan/asan_posix.cpp
index 39685696a0d0d..070ba89048f99 100644
--- a/compiler-rt/lib/asan/asan_posix.cpp
+++ b/compiler-rt/lib/asan/asan_posix.cpp
@@ -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();
diff --git a/compiler-rt/test/asan/TestCases/use-after-poison-history-size-partial-granule.cpp b/compiler-rt/test/asan/TestCases/use-after-poison-history-size-partial-granule.cpp
new file mode 100644
index 0000000000000..ec848f2677085
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/use-after-poison-history-size-partial-granule.cpp
@@ -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;
+}
diff --git a/compiler-rt/test/asan/TestCases/use-after-poison-history-size.cpp b/compiler-rt/test/asan/TestCases/use-after-poison-history-size.cpp
new file mode 100644
index 0000000000000..a279fe657b87c
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/use-after-poison-history-size.cpp
@@ -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 (
diff erent 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;
+}
More information about the llvm-commits
mailing list