[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