[compiler-rt] [compiler-rt] Expose TSAN report strings via __tsan_describe_{mop,loc,mutex,thread} (PR #200736)

Andrew Haberlandt via llvm-commits llvm-commits at lists.llvm.org
Tue Jun 2 19:13:22 PDT 2026


https://github.com/ndrewh updated https://github.com/llvm/llvm-project/pull/200736

>From 68714bc055759e0922befc0ac90dbcc8776dd7e9 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Sun, 31 May 2026 23:51:06 -0700
Subject: [PATCH 1/7] [NFC] Make ASAN/TSAN stack descriptions independent of
 report printing

This refactors the various functions for printing ASAN/TSAN report
components into PrintX and DescribeX, where DescribeX can be used
to obtain a (unformatted) string description of a report component
e.g. the string `Location is heap block of size %zu at %p allocated by %s:`.

This will make it easier to do something like #200526 downstream.
---
 compiler-rt/lib/asan/asan_descriptions.cpp |  32 +++--
 compiler-rt/lib/tsan/rtl/tsan_report.cpp   | 140 +++++++++++++--------
 2 files changed, 110 insertions(+), 62 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_descriptions.cpp b/compiler-rt/lib/asan/asan_descriptions.cpp
index 551b819a4c436..839f9a1ffae33 100644
--- a/compiler-rt/lib/asan/asan_descriptions.cpp
+++ b/compiler-rt/lib/asan/asan_descriptions.cpp
@@ -36,6 +36,25 @@ AsanThreadIdAndName::AsanThreadIdAndName(u32 tid)
   asanThreadRegistry().CheckLocked();
 }
 
+// Writes a color-free, single-line description of a heap allocation or
+// deallocation stack, e.g. "freed by thread T1 here:".
+static void DescribeAllocStack(const char *verb, AsanThreadContext *thread,
+                               InternalScopedString *s) {
+  s->AppendF("%s by thread %s here:", verb,
+             AsanThreadIdAndName(thread).c_str());
+}
+
+// Prints the description produced by DescribeAllocStack (decorated and followed
+// by a newline) and then the given stack trace.
+static void PrintAllocStack(const char *verb, AsanThreadContext *thread,
+                            const StackTrace &stack) {
+  Decorator d;
+  InternalScopedString s;
+  DescribeAllocStack(verb, thread, &s);
+  Printf("%s%s%s\n", d.Allocation(), s.data(), d.Default());
+  stack.Print();
+}
+
 // Prints this thread and, if flags()->print_full_thread_history, its ancestors
 void DescribeThread(AsanThreadContext *context) {
   while (true) {
@@ -424,21 +443,14 @@ void HeapAddressDescription::Print() const {
   AsanThreadContext *alloc_thread = GetThreadContextByTidLocked(alloc_tid);
   StackTrace alloc_stack = GetStackTraceFromId(alloc_stack_id);
 
-  Decorator d;
   AsanThreadContext *free_thread = nullptr;
   if (free_tid != kInvalidTid) {
     free_thread = GetThreadContextByTidLocked(free_tid);
-    Printf("%sfreed by thread %s here:%s\n", d.Allocation(),
-           AsanThreadIdAndName(free_thread).c_str(), d.Default());
-    StackTrace free_stack = GetStackTraceFromId(free_stack_id);
-    free_stack.Print();
-    Printf("%spreviously allocated by thread %s here:%s\n", d.Allocation(),
-           AsanThreadIdAndName(alloc_thread).c_str(), d.Default());
+    PrintAllocStack("freed", free_thread, GetStackTraceFromId(free_stack_id));
+    PrintAllocStack("previously allocated", alloc_thread, alloc_stack);
   } else {
-    Printf("%sallocated by thread %s here:%s\n", d.Allocation(),
-           AsanThreadIdAndName(alloc_thread).c_str(), d.Default());
+    PrintAllocStack("allocated", alloc_thread, alloc_stack);
   }
-  alloc_stack.Print();
   DescribeThread(GetCurrentThread());
   if (free_thread) DescribeThread(free_thread);
   DescribeThread(alloc_thread);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_report.cpp b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
index 17fd3064fb85a..c6f9c09cd2934 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_report.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
@@ -139,68 +139,88 @@ static const char *ExternalMopDesc(bool first, bool write) {
                : (write ? "Previous modifying" : "Previous read-only");
 }
 
-static void PrintMop(const ReportMop *mop, bool first) {
-  Decorator d;
+static void DescribeMop(const ReportMop *mop, bool first,
+                        InternalScopedString *s) {
   char thrbuf[kThreadBufSize];
-  Printf("%s", d.Access());
   if (mop->external_tag == kExternalTagNone) {
-    Printf("  %s of size %d at %p by %s",
-           MopDesc(first, mop->write, mop->atomic), mop->size,
-           (void *)mop->addr, thread_name(thrbuf, mop->tid));
+    s->AppendF("  %s of size %d at %p by %s",
+               MopDesc(first, mop->write, mop->atomic), mop->size,
+               (void *)mop->addr, thread_name(thrbuf, mop->tid));
   } else {
     const char *object_type = GetObjectTypeFromTag(mop->external_tag);
     if (object_type == nullptr)
         object_type = "external object";
-    Printf("  %s access of %s at %p by %s",
-           ExternalMopDesc(first, mop->write), object_type,
-           (void *)mop->addr, thread_name(thrbuf, mop->tid));
+    s->AppendF("  %s access of %s at %p by %s",
+               ExternalMopDesc(first, mop->write), object_type,
+               (void *)mop->addr, thread_name(thrbuf, mop->tid));
   }
+}
+
+static void PrintMop(const ReportMop *mop, bool first) {
+  Decorator d;
+  Printf("%s", d.Access());
+  InternalScopedString s;
+  DescribeMop(mop, first, &s);
+  Printf("%s", s.data());
   PrintMutexSet(mop->mset);
   Printf(":\n");
   Printf("%s", d.Default());
   PrintStack(mop->stack);
 }
 
-static void PrintLocation(const ReportLocation *loc) {
-  Decorator d;
+// Writes the (uncolored) location description and returns whether a stack
+// trace should follow (true only for Heap and FD locations).
+static bool DescribeLocation(const ReportLocation *loc,
+                             InternalScopedString *s) {
   char thrbuf[kThreadBufSize];
   bool print_stack = false;
-  Printf("%s", d.Location());
   if (loc->type == ReportLocationGlobal) {
     const DataInfo &global = loc->global;
     if (global.size != 0)
-      Printf("  Location is global '%s' of size %zu at %p (%s+0x%zx)\n\n",
-             global.name, global.size, reinterpret_cast<void *>(global.start),
-             StripModuleName(global.module), global.module_offset);
+      s->AppendF("  Location is global '%s' of size %zu at %p (%s+0x%zx)",
+                 global.name, global.size,
+                 reinterpret_cast<void *>(global.start),
+                 StripModuleName(global.module), global.module_offset);
     else
-      Printf("  Location is global '%s' at %p (%s+0x%zx)\n\n", global.name,
-             reinterpret_cast<void *>(global.start),
-             StripModuleName(global.module), global.module_offset);
+      s->AppendF("  Location is global '%s' at %p (%s+0x%zx)", global.name,
+                 reinterpret_cast<void *>(global.start),
+                 StripModuleName(global.module), global.module_offset);
   } else if (loc->type == ReportLocationHeap) {
-    char thrbuf[kThreadBufSize];
     const char *object_type = GetObjectTypeFromTag(loc->external_tag);
     if (!object_type) {
-      Printf("  Location is heap block of size %zu at %p allocated by %s:\n",
-             loc->heap_chunk_size,
-             reinterpret_cast<void *>(loc->heap_chunk_start),
-             thread_name(thrbuf, loc->tid));
+      s->AppendF("  Location is heap block of size %zu at %p allocated by %s:",
+                 loc->heap_chunk_size,
+                 reinterpret_cast<void *>(loc->heap_chunk_start),
+                 thread_name(thrbuf, loc->tid));
     } else {
-      Printf("  Location is %s of size %zu at %p allocated by %s:\n",
-             object_type, loc->heap_chunk_size,
-             reinterpret_cast<void *>(loc->heap_chunk_start),
-             thread_name(thrbuf, loc->tid));
+      s->AppendF("  Location is %s of size %zu at %p allocated by %s:",
+                 object_type, loc->heap_chunk_size,
+                 reinterpret_cast<void *>(loc->heap_chunk_start),
+                 thread_name(thrbuf, loc->tid));
     }
     print_stack = true;
   } else if (loc->type == ReportLocationStack) {
-    Printf("  Location is stack of %s.\n\n", thread_name(thrbuf, loc->tid));
+    s->AppendF("  Location is stack of %s.", thread_name(thrbuf, loc->tid));
   } else if (loc->type == ReportLocationTLS) {
-    Printf("  Location is TLS of %s.\n\n", thread_name(thrbuf, loc->tid));
+    s->AppendF("  Location is TLS of %s.", thread_name(thrbuf, loc->tid));
   } else if (loc->type == ReportLocationFD) {
-    Printf("  Location is file descriptor %d %s by %s at:\n", loc->fd,
-           loc->fd_closed ? "destroyed" : "created",
-           thread_name(thrbuf, loc->tid));
+    s->AppendF("  Location is file descriptor %d %s by %s at:", loc->fd,
+               loc->fd_closed ? "destroyed" : "created",
+               thread_name(thrbuf, loc->tid));
     print_stack = true;
   }
+  return print_stack;
+}
+
+static void PrintLocation(const ReportLocation *loc) {
+  Decorator d;
+  Printf("%s", d.Location());
+  InternalScopedString s;
+  bool print_stack = DescribeLocation(loc, &s);
+  Printf("%s", s.data());
+  // Locations with a following stack are separated from it by a single
+  // newline; the others provide their own trailing blank line.
+  Printf(print_stack ? "\n" : "\n\n");
   Printf("%s", d.Default());
   if (print_stack)
     PrintStack(loc->stack);
@@ -218,39 +238,55 @@ static void PrintMutexShortWithAddress(const ReportMutex *rm,
          reinterpret_cast<void *>(rm->addr), d.Default(), after);
 }
 
+static void DescribeMutex(const ReportMutex *rm, InternalScopedString *s) {
+  s->AppendF("  Mutex M%u (%p) created at:", rm->id,
+             reinterpret_cast<void *>(rm->addr));
+}
+
 static void PrintMutex(const ReportMutex *rm) {
   Decorator d;
   Printf("%s", d.Mutex());
-  Printf("  Mutex M%u (%p) created at:\n", rm->id,
-         reinterpret_cast<void *>(rm->addr));
+  InternalScopedString s;
+  DescribeMutex(rm, &s);
+  Printf("%s\n", s.data());
   Printf("%s", d.Default());
   PrintStack(rm->stack);
 }
 
-static void PrintThread(const ReportThread *rt) {
-  Decorator d;
-  if (rt->id == kMainTid)  // Little sense in describing the main thread.
-    return;
-  Printf("%s", d.ThreadDescription());
-  Printf("  Thread T%d", rt->id);
-  if (rt->name && rt->name[0] != '\0')
-    Printf(" '%s'", rt->name);
+// Writes the (uncolored) description for a non-main thread and returns whether
+// a stack trace should follow (false for GCD worker threads).
+static bool DescribeThread(const ReportThread *rt, InternalScopedString *s) {
   char thrbuf[kThreadBufSize];
+  s->AppendF("  Thread T%d", rt->id);
+  if (rt->name && rt->name[0] != '\0')
+    s->AppendF(" '%s'", rt->name);
   const char *thread_status = rt->running ? "running" : "finished";
   if (rt->thread_type == ThreadType::Worker) {
-    Printf(" (tid=%llu, %s) is a GCD worker thread\n", rt->os_id,
-           thread_status);
-    Printf("\n");
-    Printf("%s", d.Default());
-    return;
+    s->AppendF(" (tid=%llu, %s) is a GCD worker thread", rt->os_id,
+               thread_status);
+    return false;
   }
-  Printf(" (tid=%llu, %s) created by %s", rt->os_id, thread_status,
-         thread_name(thrbuf, rt->parent_tid));
+  s->AppendF(" (tid=%llu, %s) created by %s", rt->os_id, thread_status,
+             thread_name(thrbuf, rt->parent_tid));
   if (rt->stack)
-    Printf(" at:");
-  Printf("\n");
+    s->Append(" at:");
+  return true;
+}
+
+static void PrintThread(const ReportThread *rt) {
+  Decorator d;
+  if (rt->id == kMainTid)  // Little sense in describing the main thread.
+    return;
+  Printf("%s", d.ThreadDescription());
+  InternalScopedString s;
+  bool print_stack = DescribeThread(rt, &s);
+  Printf("%s", s.data());
+  // A thread with a following stack is separated from it by a single newline;
+  // otherwise we emit the trailing blank line here.
+  Printf(print_stack ? "\n" : "\n\n");
   Printf("%s", d.Default());
-  PrintStack(rt->stack);
+  if (print_stack)
+    PrintStack(rt->stack);
 }
 
 static void PrintSleep(const ReportStack *s) {

>From 779cb99ead6f0de2c1ef7c30adc942d9bd8d99b2 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Mon, 1 Jun 2026 00:10:59 -0700
Subject: [PATCH 2/7] format

---
 compiler-rt/lib/asan/asan_descriptions.cpp |  8 ++---
 compiler-rt/lib/tsan/rtl/tsan_report.cpp   | 38 +++++++++++-----------
 2 files changed, 23 insertions(+), 23 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_descriptions.cpp b/compiler-rt/lib/asan/asan_descriptions.cpp
index 839f9a1ffae33..98d7b44085fce 100644
--- a/compiler-rt/lib/asan/asan_descriptions.cpp
+++ b/compiler-rt/lib/asan/asan_descriptions.cpp
@@ -38,16 +38,16 @@ AsanThreadIdAndName::AsanThreadIdAndName(u32 tid)
 
 // Writes a color-free, single-line description of a heap allocation or
 // deallocation stack, e.g. "freed by thread T1 here:".
-static void DescribeAllocStack(const char *verb, AsanThreadContext *thread,
-                               InternalScopedString *s) {
+static void DescribeAllocStack(const char* verb, AsanThreadContext* thread,
+                               InternalScopedString* s) {
   s->AppendF("%s by thread %s here:", verb,
              AsanThreadIdAndName(thread).c_str());
 }
 
 // Prints the description produced by DescribeAllocStack (decorated and followed
 // by a newline) and then the given stack trace.
-static void PrintAllocStack(const char *verb, AsanThreadContext *thread,
-                            const StackTrace &stack) {
+static void PrintAllocStack(const char* verb, AsanThreadContext* thread,
+                            const StackTrace& stack) {
   Decorator d;
   InternalScopedString s;
   DescribeAllocStack(verb, thread, &s);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_report.cpp b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
index c6f9c09cd2934..eb76c2ff77e13 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_report.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
@@ -139,24 +139,24 @@ static const char *ExternalMopDesc(bool first, bool write) {
                : (write ? "Previous modifying" : "Previous read-only");
 }
 
-static void DescribeMop(const ReportMop *mop, bool first,
-                        InternalScopedString *s) {
+static void DescribeMop(const ReportMop* mop, bool first,
+                        InternalScopedString* s) {
   char thrbuf[kThreadBufSize];
   if (mop->external_tag == kExternalTagNone) {
     s->AppendF("  %s of size %d at %p by %s",
                MopDesc(first, mop->write, mop->atomic), mop->size,
-               (void *)mop->addr, thread_name(thrbuf, mop->tid));
+               (void*)mop->addr, thread_name(thrbuf, mop->tid));
   } else {
     const char *object_type = GetObjectTypeFromTag(mop->external_tag);
     if (object_type == nullptr)
         object_type = "external object";
     s->AppendF("  %s access of %s at %p by %s",
                ExternalMopDesc(first, mop->write), object_type,
-               (void *)mop->addr, thread_name(thrbuf, mop->tid));
+               (void*)mop->addr, thread_name(thrbuf, mop->tid));
   }
 }
 
-static void PrintMop(const ReportMop *mop, bool first) {
+static void PrintMop(const ReportMop* mop, bool first) {
   Decorator d;
   Printf("%s", d.Access());
   InternalScopedString s;
@@ -170,8 +170,8 @@ static void PrintMop(const ReportMop *mop, bool first) {
 
 // Writes the (uncolored) location description and returns whether a stack
 // trace should follow (true only for Heap and FD locations).
-static bool DescribeLocation(const ReportLocation *loc,
-                             InternalScopedString *s) {
+static bool DescribeLocation(const ReportLocation* loc,
+                             InternalScopedString* s) {
   char thrbuf[kThreadBufSize];
   bool print_stack = false;
   if (loc->type == ReportLocationGlobal) {
@@ -179,24 +179,24 @@ static bool DescribeLocation(const ReportLocation *loc,
     if (global.size != 0)
       s->AppendF("  Location is global '%s' of size %zu at %p (%s+0x%zx)",
                  global.name, global.size,
-                 reinterpret_cast<void *>(global.start),
+                 reinterpret_cast<void*>(global.start),
                  StripModuleName(global.module), global.module_offset);
     else
       s->AppendF("  Location is global '%s' at %p (%s+0x%zx)", global.name,
-                 reinterpret_cast<void *>(global.start),
+                 reinterpret_cast<void*>(global.start),
                  StripModuleName(global.module), global.module_offset);
   } else if (loc->type == ReportLocationHeap) {
     const char *object_type = GetObjectTypeFromTag(loc->external_tag);
     if (!object_type) {
       s->AppendF("  Location is heap block of size %zu at %p allocated by %s:",
                  loc->heap_chunk_size,
-                 reinterpret_cast<void *>(loc->heap_chunk_start),
+                 reinterpret_cast<void*>(loc->heap_chunk_start),
                  thread_name(thrbuf, loc->tid));
     } else {
-      s->AppendF("  Location is %s of size %zu at %p allocated by %s:",
-                 object_type, loc->heap_chunk_size,
-                 reinterpret_cast<void *>(loc->heap_chunk_start),
-                 thread_name(thrbuf, loc->tid));
+      s->AppendF(
+          "  Location is %s of size %zu at %p allocated by %s:", object_type,
+          loc->heap_chunk_size, reinterpret_cast<void*>(loc->heap_chunk_start),
+          thread_name(thrbuf, loc->tid));
     }
     print_stack = true;
   } else if (loc->type == ReportLocationStack) {
@@ -212,7 +212,7 @@ static bool DescribeLocation(const ReportLocation *loc,
   return print_stack;
 }
 
-static void PrintLocation(const ReportLocation *loc) {
+static void PrintLocation(const ReportLocation* loc) {
   Decorator d;
   Printf("%s", d.Location());
   InternalScopedString s;
@@ -238,9 +238,9 @@ static void PrintMutexShortWithAddress(const ReportMutex *rm,
          reinterpret_cast<void *>(rm->addr), d.Default(), after);
 }
 
-static void DescribeMutex(const ReportMutex *rm, InternalScopedString *s) {
+static void DescribeMutex(const ReportMutex* rm, InternalScopedString* s) {
   s->AppendF("  Mutex M%u (%p) created at:", rm->id,
-             reinterpret_cast<void *>(rm->addr));
+             reinterpret_cast<void*>(rm->addr));
 }
 
 static void PrintMutex(const ReportMutex *rm) {
@@ -255,7 +255,7 @@ static void PrintMutex(const ReportMutex *rm) {
 
 // Writes the (uncolored) description for a non-main thread and returns whether
 // a stack trace should follow (false for GCD worker threads).
-static bool DescribeThread(const ReportThread *rt, InternalScopedString *s) {
+static bool DescribeThread(const ReportThread* rt, InternalScopedString* s) {
   char thrbuf[kThreadBufSize];
   s->AppendF("  Thread T%d", rt->id);
   if (rt->name && rt->name[0] != '\0')
@@ -273,7 +273,7 @@ static bool DescribeThread(const ReportThread *rt, InternalScopedString *s) {
   return true;
 }
 
-static void PrintThread(const ReportThread *rt) {
+static void PrintThread(const ReportThread* rt) {
   Decorator d;
   if (rt->id == kMainTid)  // Little sense in describing the main thread.
     return;

>From 4221cda5d0e13cd0bc91465ff6a682877b1baca4 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Mon, 1 Jun 2026 12:10:16 -0700
Subject: [PATCH 3/7] Test DescribeX functions; use reference output params

---
 compiler-rt/lib/tsan/rtl/tsan_report.cpp      |  81 +++++-----
 compiler-rt/lib/tsan/rtl/tsan_report.h        |  13 ++
 .../lib/tsan/tests/unit/CMakeLists.txt        |   1 +
 .../lib/tsan/tests/unit/tsan_report_test.cpp  | 152 ++++++++++++++++++
 4 files changed, 205 insertions(+), 42 deletions(-)
 create mode 100644 compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp

diff --git a/compiler-rt/lib/tsan/rtl/tsan_report.cpp b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
index eb76c2ff77e13..19d39886aa7b6 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_report.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_report.cpp
@@ -139,20 +139,19 @@ static const char *ExternalMopDesc(bool first, bool write) {
                : (write ? "Previous modifying" : "Previous read-only");
 }
 
-static void DescribeMop(const ReportMop* mop, bool first,
-                        InternalScopedString* s) {
+void DescribeMop(const ReportMop* mop, bool first, InternalScopedString& s) {
   char thrbuf[kThreadBufSize];
   if (mop->external_tag == kExternalTagNone) {
-    s->AppendF("  %s of size %d at %p by %s",
-               MopDesc(first, mop->write, mop->atomic), mop->size,
-               (void*)mop->addr, thread_name(thrbuf, mop->tid));
+    s.AppendF("  %s of size %d at %p by %s",
+              MopDesc(first, mop->write, mop->atomic), mop->size,
+              (void*)mop->addr, thread_name(thrbuf, mop->tid));
   } else {
     const char *object_type = GetObjectTypeFromTag(mop->external_tag);
     if (object_type == nullptr)
         object_type = "external object";
-    s->AppendF("  %s access of %s at %p by %s",
-               ExternalMopDesc(first, mop->write), object_type,
-               (void*)mop->addr, thread_name(thrbuf, mop->tid));
+    s.AppendF("  %s access of %s at %p by %s",
+              ExternalMopDesc(first, mop->write), object_type, (void*)mop->addr,
+              thread_name(thrbuf, mop->tid));
   }
 }
 
@@ -160,7 +159,7 @@ static void PrintMop(const ReportMop* mop, bool first) {
   Decorator d;
   Printf("%s", d.Access());
   InternalScopedString s;
-  DescribeMop(mop, first, &s);
+  DescribeMop(mop, first, s);
   Printf("%s", s.data());
   PrintMutexSet(mop->mset);
   Printf(":\n");
@@ -170,43 +169,41 @@ static void PrintMop(const ReportMop* mop, bool first) {
 
 // Writes the (uncolored) location description and returns whether a stack
 // trace should follow (true only for Heap and FD locations).
-static bool DescribeLocation(const ReportLocation* loc,
-                             InternalScopedString* s) {
+bool DescribeLocation(const ReportLocation* loc, InternalScopedString& s) {
   char thrbuf[kThreadBufSize];
   bool print_stack = false;
   if (loc->type == ReportLocationGlobal) {
     const DataInfo &global = loc->global;
     if (global.size != 0)
-      s->AppendF("  Location is global '%s' of size %zu at %p (%s+0x%zx)",
-                 global.name, global.size,
-                 reinterpret_cast<void*>(global.start),
-                 StripModuleName(global.module), global.module_offset);
+      s.AppendF("  Location is global '%s' of size %zu at %p (%s+0x%zx)",
+                global.name, global.size, reinterpret_cast<void*>(global.start),
+                StripModuleName(global.module), global.module_offset);
     else
-      s->AppendF("  Location is global '%s' at %p (%s+0x%zx)", global.name,
-                 reinterpret_cast<void*>(global.start),
-                 StripModuleName(global.module), global.module_offset);
+      s.AppendF("  Location is global '%s' at %p (%s+0x%zx)", global.name,
+                reinterpret_cast<void*>(global.start),
+                StripModuleName(global.module), global.module_offset);
   } else if (loc->type == ReportLocationHeap) {
     const char *object_type = GetObjectTypeFromTag(loc->external_tag);
     if (!object_type) {
-      s->AppendF("  Location is heap block of size %zu at %p allocated by %s:",
-                 loc->heap_chunk_size,
-                 reinterpret_cast<void*>(loc->heap_chunk_start),
-                 thread_name(thrbuf, loc->tid));
+      s.AppendF("  Location is heap block of size %zu at %p allocated by %s:",
+                loc->heap_chunk_size,
+                reinterpret_cast<void*>(loc->heap_chunk_start),
+                thread_name(thrbuf, loc->tid));
     } else {
-      s->AppendF(
+      s.AppendF(
           "  Location is %s of size %zu at %p allocated by %s:", object_type,
           loc->heap_chunk_size, reinterpret_cast<void*>(loc->heap_chunk_start),
           thread_name(thrbuf, loc->tid));
     }
     print_stack = true;
   } else if (loc->type == ReportLocationStack) {
-    s->AppendF("  Location is stack of %s.", thread_name(thrbuf, loc->tid));
+    s.AppendF("  Location is stack of %s.", thread_name(thrbuf, loc->tid));
   } else if (loc->type == ReportLocationTLS) {
-    s->AppendF("  Location is TLS of %s.", thread_name(thrbuf, loc->tid));
+    s.AppendF("  Location is TLS of %s.", thread_name(thrbuf, loc->tid));
   } else if (loc->type == ReportLocationFD) {
-    s->AppendF("  Location is file descriptor %d %s by %s at:", loc->fd,
-               loc->fd_closed ? "destroyed" : "created",
-               thread_name(thrbuf, loc->tid));
+    s.AppendF("  Location is file descriptor %d %s by %s at:", loc->fd,
+              loc->fd_closed ? "destroyed" : "created",
+              thread_name(thrbuf, loc->tid));
     print_stack = true;
   }
   return print_stack;
@@ -216,7 +213,7 @@ static void PrintLocation(const ReportLocation* loc) {
   Decorator d;
   Printf("%s", d.Location());
   InternalScopedString s;
-  bool print_stack = DescribeLocation(loc, &s);
+  bool print_stack = DescribeLocation(loc, s);
   Printf("%s", s.data());
   // Locations with a following stack are separated from it by a single
   // newline; the others provide their own trailing blank line.
@@ -238,16 +235,16 @@ static void PrintMutexShortWithAddress(const ReportMutex *rm,
          reinterpret_cast<void *>(rm->addr), d.Default(), after);
 }
 
-static void DescribeMutex(const ReportMutex* rm, InternalScopedString* s) {
-  s->AppendF("  Mutex M%u (%p) created at:", rm->id,
-             reinterpret_cast<void*>(rm->addr));
+void DescribeMutex(const ReportMutex* rm, InternalScopedString& s) {
+  s.AppendF("  Mutex M%u (%p) created at:", rm->id,
+            reinterpret_cast<void*>(rm->addr));
 }
 
 static void PrintMutex(const ReportMutex *rm) {
   Decorator d;
   Printf("%s", d.Mutex());
   InternalScopedString s;
-  DescribeMutex(rm, &s);
+  DescribeMutex(rm, s);
   Printf("%s\n", s.data());
   Printf("%s", d.Default());
   PrintStack(rm->stack);
@@ -255,21 +252,21 @@ static void PrintMutex(const ReportMutex *rm) {
 
 // Writes the (uncolored) description for a non-main thread and returns whether
 // a stack trace should follow (false for GCD worker threads).
-static bool DescribeThread(const ReportThread* rt, InternalScopedString* s) {
+bool DescribeThread(const ReportThread* rt, InternalScopedString& s) {
   char thrbuf[kThreadBufSize];
-  s->AppendF("  Thread T%d", rt->id);
+  s.AppendF("  Thread T%d", rt->id);
   if (rt->name && rt->name[0] != '\0')
-    s->AppendF(" '%s'", rt->name);
+    s.AppendF(" '%s'", rt->name);
   const char *thread_status = rt->running ? "running" : "finished";
   if (rt->thread_type == ThreadType::Worker) {
-    s->AppendF(" (tid=%llu, %s) is a GCD worker thread", rt->os_id,
-               thread_status);
+    s.AppendF(" (tid=%llu, %s) is a GCD worker thread", rt->os_id,
+              thread_status);
     return false;
   }
-  s->AppendF(" (tid=%llu, %s) created by %s", rt->os_id, thread_status,
-             thread_name(thrbuf, rt->parent_tid));
+  s.AppendF(" (tid=%llu, %s) created by %s", rt->os_id, thread_status,
+            thread_name(thrbuf, rt->parent_tid));
   if (rt->stack)
-    s->Append(" at:");
+    s.Append(" at:");
   return true;
 }
 
@@ -279,7 +276,7 @@ static void PrintThread(const ReportThread* rt) {
     return;
   Printf("%s", d.ThreadDescription());
   InternalScopedString s;
-  bool print_stack = DescribeThread(rt, &s);
+  bool print_stack = DescribeThread(rt, s);
   Printf("%s", s.data());
   // A thread with a following stack is separated from it by a single newline;
   // otherwise we emit the trailing blank line here.
diff --git a/compiler-rt/lib/tsan/rtl/tsan_report.h b/compiler-rt/lib/tsan/rtl/tsan_report.h
index 53bb21964dbba..b3321894ea2cd 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_report.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_report.h
@@ -137,6 +137,19 @@ class ReportDesc {
 void PrintReport(const ReportDesc *rep);
 void PrintStack(const ReportStack *stack);
 
+// Write an uncolored, single-line (newline-free) description of a report
+// component into `s`. These are the formatting cores shared with the PrintX
+// functions; they perform no decoration, add no trailing newline, and print no
+// stack trace, so they can be reused to obtain the raw description string.
+//
+// DescribeLocation and DescribeThread return whether a stack trace logically
+// follows the description (e.g. false for non-heap locations and GCD worker
+// threads).
+void DescribeMop(const ReportMop* mop, bool first, InternalScopedString& s);
+bool DescribeLocation(const ReportLocation* loc, InternalScopedString& s);
+void DescribeMutex(const ReportMutex* rm, InternalScopedString& s);
+bool DescribeThread(const ReportThread* rt, InternalScopedString& s);
+
 }  // namespace __tsan
 
 #endif  // TSAN_REPORT_H
diff --git a/compiler-rt/lib/tsan/tests/unit/CMakeLists.txt b/compiler-rt/lib/tsan/tests/unit/CMakeLists.txt
index d183c3071cff8..cc8055c603e3c 100644
--- a/compiler-rt/lib/tsan/tests/unit/CMakeLists.txt
+++ b/compiler-rt/lib/tsan/tests/unit/CMakeLists.txt
@@ -4,6 +4,7 @@ set(TSAN_UNIT_TEST_SOURCES
   tsan_ilist_test.cpp
   tsan_mman_test.cpp
   tsan_percent_test.cpp
+  tsan_report_test.cpp
   tsan_shadow_test.cpp
   tsan_stack_test.cpp
   tsan_sync_test.cpp
diff --git a/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp b/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp
new file mode 100644
index 0000000000000..2150f16261859
--- /dev/null
+++ b/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp
@@ -0,0 +1,152 @@
+//===-- tsan_report_test.cpp ----------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file is a part of ThreadSanitizer (TSan), a race detector.
+//
+// Tests for the DescribeX functions in tsan_report.cpp, which produce the
+// uncolored, newline-free description strings shared with the PrintX path.
+//
+// NOTE: The exact string format asserted in these tests is not a stable
+// contract. These checks just pin down the current output so the DescribeX /
+// PrintX split stays in sync; if the report wording changes, update the
+// expected strings here to match.
+//
+//===----------------------------------------------------------------------===//
+#include "tsan_report.h"
+
+#include "gtest/gtest.h"
+
+namespace __tsan {
+
+TEST(Report, DescribeMop) {
+  ReportMop mop;
+  mop.tid = 1;
+  mop.addr = 0x1234;
+  mop.size = 4;
+  mop.write = true;
+  mop.atomic = false;
+  mop.external_tag = kExternalTagNone;
+
+  InternalScopedString s;
+  DescribeMop(&mop, /*first=*/true, s);
+  EXPECT_STREQ("  Write of size 4 at 0x1234 by thread T1", s.data());
+
+  s.clear();
+  mop.write = false;
+  DescribeMop(&mop, /*first=*/false, s);
+  EXPECT_STREQ("  Previous read of size 4 at 0x1234 by thread T1", s.data());
+
+  s.clear();
+  mop.write = true;
+  mop.atomic = true;
+  mop.tid = kMainTid;
+  DescribeMop(&mop, /*first=*/true, s);
+  EXPECT_STREQ("  Atomic write of size 4 at 0x1234 by main thread", s.data());
+}
+
+TEST(Report, DescribeLocationStackAndTls) {
+  ReportLocation loc;
+  loc.tid = 2;
+
+  loc.type = ReportLocationStack;
+  InternalScopedString s;
+  // Stack/TLS locations carry no following stack trace.
+  EXPECT_FALSE(DescribeLocation(&loc, s));
+  EXPECT_STREQ("  Location is stack of thread T2.", s.data());
+
+  loc.type = ReportLocationTLS;
+  s.clear();
+  EXPECT_FALSE(DescribeLocation(&loc, s));
+  EXPECT_STREQ("  Location is TLS of thread T2.", s.data());
+}
+
+TEST(Report, DescribeLocationHeap) {
+  ReportLocation loc;
+  loc.type = ReportLocationHeap;
+  loc.tid = 3;
+  loc.heap_chunk_start = 0x4000;
+  loc.heap_chunk_size = 16;
+  loc.external_tag = kExternalTagNone;
+
+  InternalScopedString s;
+  // Heap locations are followed by the allocation stack.
+  EXPECT_TRUE(DescribeLocation(&loc, s));
+  EXPECT_STREQ(
+      "  Location is heap block of size 16 at 0x4000 allocated by thread T3:",
+      s.data());
+}
+
+TEST(Report, DescribeLocationFD) {
+  ReportLocation loc;
+  loc.type = ReportLocationFD;
+  loc.tid = kMainTid;
+  loc.fd = 7;
+
+  InternalScopedString s;
+  loc.fd_closed = false;
+  EXPECT_TRUE(DescribeLocation(&loc, s));
+  EXPECT_STREQ("  Location is file descriptor 7 created by main thread at:",
+               s.data());
+
+  s.clear();
+  loc.fd_closed = true;
+  EXPECT_TRUE(DescribeLocation(&loc, s));
+  EXPECT_STREQ("  Location is file descriptor 7 destroyed by main thread at:",
+               s.data());
+}
+
+TEST(Report, DescribeMutex) {
+  ReportMutex rm;
+  rm.id = 5;
+  rm.addr = 0xdead;
+
+  InternalScopedString s;
+  DescribeMutex(&rm, s);
+  EXPECT_STREQ("  Mutex M5 (0xdead) created at:", s.data());
+}
+
+TEST(Report, DescribeThread) {
+  ReportThread rt = {};
+  rt.id = 1;
+  rt.os_id = 42;
+  rt.running = true;
+  rt.thread_type = ThreadType::Regular;
+  rt.name = nullptr;
+  rt.parent_tid = kMainTid;
+  rt.stack = nullptr;
+
+  InternalScopedString s;
+  // A regular thread is followed by its creation stack.
+  EXPECT_TRUE(DescribeThread(&rt, s));
+  EXPECT_STREQ("  Thread T1 (tid=42, running) created by main thread",
+               s.data());
+
+  // A name is included and " at:" is appended when a stack is present.
+  s.clear();
+  char name[] = "worker";
+  rt.name = name;
+  rt.running = false;
+  ReportStack stack;
+  rt.stack = &stack;
+  EXPECT_TRUE(DescribeThread(&rt, s));
+  EXPECT_STREQ(
+      "  Thread T1 'worker' (tid=42, finished) created by main thread at:",
+      s.data());
+
+  // GCD worker threads have no following stack.
+  s.clear();
+  rt.name = nullptr;
+  rt.running = true;
+  rt.stack = nullptr;
+  rt.thread_type = ThreadType::Worker;
+  EXPECT_FALSE(DescribeThread(&rt, s));
+  EXPECT_STREQ("  Thread T1 (tid=42, running) is a GCD worker thread",
+               s.data());
+}
+
+}  // namespace __tsan

>From 15c6457fe46a95ec6f2394ab739eb00d3ac0d508 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 2 Jun 2026 17:34:17 -0700
Subject: [PATCH 4/7] [tsan] Add __tsan_describe_{mop,loc,mutex,thread}
 accessors

Expose the existing DescribeMop / DescribeLocation / DescribeMutex /
DescribeThread helpers (made public in the prior NFC) through the public
tsan_interface.h alongside the structured __tsan_get_report_* accessors,
so out-of-process consumers (e.g. crash-reporting integrations) can
obtain the same uncolored, single-line component descriptions the report
printer uses without re-implementing the formatting logic.

The describe_loc and describe_thread variants additionally return whether
a stack trace logically follows the description (matching the bool
returned by the underlying internal helpers), so callers know when to
pair the description with the stack returned by __tsan_get_report_loc /
__tsan_get_report_thread.

Assisted-by: Claude
---
 .../include/sanitizer/tsan_interface.h        |  54 +++++++++
 compiler-rt/lib/tsan/rtl/tsan_debugging.cpp   |  52 ++++++++
 compiler-rt/lib/tsan/rtl/tsan_interface.h     |  14 +++
 compiler-rt/test/tsan/debug_describe.cpp      | 114 ++++++++++++++++++
 4 files changed, 234 insertions(+)
 create mode 100644 compiler-rt/test/tsan/debug_describe.cpp

diff --git a/compiler-rt/include/sanitizer/tsan_interface.h b/compiler-rt/include/sanitizer/tsan_interface.h
index e11a4175cd8ed..b2fcdfae11127 100644
--- a/compiler-rt/include/sanitizer/tsan_interface.h
+++ b/compiler-rt/include/sanitizer/tsan_interface.h
@@ -309,6 +309,60 @@ int SANITIZER_CDECL __tsan_get_report_unique_tid(void *report,
 /// \returns An opaque pointer to the current report. Otherwise returns NULL.
 void *SANITIZER_CDECL __tsan_get_current_report();
 
+/// Writes a single-line, uncolored description of a memory operation in the
+/// report into <c>out</c> (e.g. <c>"  Write of size 4 at 0x... by thread T1"</c>).
+///
+/// The output is null-terminated and truncated to fit <c>outlen</c> bytes
+/// (including the terminator).
+///
+/// \param report Opaque pointer to the current report.
+/// \param idx Index of the memory operation (same indexing as
+///            __tsan_get_report_mop).
+/// \param first Non-zero if this is the first / primary memory operation
+///              in the report; controls "Write" vs "Previous write" wording.
+/// \param out Buffer to write the description into.
+/// \param outlen Capacity of <c>out</c> in bytes.
+/// \returns Returns 1 on success, 0 on failure.
+int SANITIZER_CDECL __tsan_describe_mop(void *report, unsigned long idx,
+                                        int first, char *out,
+                                        unsigned long outlen);
+
+/// Writes a single-line, uncolored description of a location in the report
+/// into <c>out</c>.
+///
+/// \param report Opaque pointer to the current report.
+/// \param idx Index of the location (same indexing as __tsan_get_report_loc).
+/// \param out Buffer to write the description into.
+/// \param outlen Capacity of <c>out</c> in bytes.
+/// \returns Returns 1 if a stack trace logically follows this description
+///          (true for heap and file-descriptor locations), 0 otherwise.
+int SANITIZER_CDECL __tsan_describe_loc(void *report, unsigned long idx,
+                                        char *out, unsigned long outlen);
+
+/// Writes a single-line, uncolored description of a mutex in the report
+/// into <c>out</c>.
+///
+/// \param report Opaque pointer to the current report.
+/// \param idx Index of the mutex (same indexing as __tsan_get_report_mutex).
+/// \param out Buffer to write the description into.
+/// \param outlen Capacity of <c>out</c> in bytes.
+/// \returns Returns 1 on success, 0 on failure.
+int SANITIZER_CDECL __tsan_describe_mutex(void *report, unsigned long idx,
+                                          char *out, unsigned long outlen);
+
+/// Writes a single-line, uncolored description of a thread in the report
+/// into <c>out</c>.
+///
+/// \param report Opaque pointer to the current report.
+/// \param idx Index of the thread (same indexing as __tsan_get_report_thread).
+/// \param out Buffer to write the description into.
+/// \param outlen Capacity of <c>out</c> in bytes.
+/// \returns Returns 1 if a stack trace logically follows this description
+///          (creation stack of the thread), 0 otherwise (e.g. main thread,
+///          GCD worker thread).
+int SANITIZER_CDECL __tsan_describe_thread(void *report, unsigned long idx,
+                                           char *out, unsigned long outlen);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif
diff --git a/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp b/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
index b3422af756067..ee700a2afbc8c 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
@@ -188,6 +188,58 @@ int __tsan_get_report_unique_tid(void *report, uptr idx, int *tid) {
   return 1;
 }
 
+namespace {
+// Copy `s` (null-terminated) to `out`, truncating to `outlen-1` bytes plus
+// a null terminator. No-op when outlen is zero.
+void CopyDescription(const InternalScopedString &s, char *out, uptr outlen) {
+  if (!out || !outlen) return;
+  uptr n = Min(s.length(), outlen - 1);
+  internal_memcpy(out, s.data(), n);
+  out[n] = '\0';
+}
+}  // namespace
+
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_mop(void *report, uptr idx, int first, char *out,
+                        uptr outlen) {
+  const ReportDesc *rep = (ReportDesc *)report;
+  CHECK_LT(idx, rep->mops.Size());
+  InternalScopedString s;
+  DescribeMop(rep->mops[idx], first != 0, s);
+  CopyDescription(s, out, outlen);
+  return 1;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_loc(void *report, uptr idx, char *out, uptr outlen) {
+  const ReportDesc *rep = (ReportDesc *)report;
+  CHECK_LT(idx, rep->locs.Size());
+  InternalScopedString s;
+  bool stack_follows = DescribeLocation(rep->locs[idx], s);
+  CopyDescription(s, out, outlen);
+  return stack_follows ? 1 : 0;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_mutex(void *report, uptr idx, char *out, uptr outlen) {
+  const ReportDesc *rep = (ReportDesc *)report;
+  CHECK_LT(idx, rep->mutexes.Size());
+  InternalScopedString s;
+  DescribeMutex(rep->mutexes[idx], s);
+  CopyDescription(s, out, outlen);
+  return 1;
+}
+
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_thread(void *report, uptr idx, char *out, uptr outlen) {
+  const ReportDesc *rep = (ReportDesc *)report;
+  CHECK_LT(idx, rep->threads.Size());
+  InternalScopedString s;
+  bool stack_follows = DescribeThread(rep->threads[idx], s);
+  CopyDescription(s, out, outlen);
+  return stack_follows ? 1 : 0;
+}
+
 SANITIZER_INTERFACE_ATTRIBUTE
 const char *__tsan_locate_address(uptr addr, char *name, uptr name_size,
                                   uptr *region_address_ptr,
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interface.h b/compiler-rt/lib/tsan/rtl/tsan_interface.h
index db94cf48f9c2d..b759fc1d10db6 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interface.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_interface.h
@@ -183,6 +183,20 @@ int __tsan_get_report_thread(void *report, uptr idx, int *tid, ThreadID *os_id,
 SANITIZER_INTERFACE_ATTRIBUTE
 int __tsan_get_report_unique_tid(void *report, uptr idx, int *tid);
 
+// Writes uncolored, single-line descriptions of report components into a
+// caller-provided buffer. Output is truncated to fit. The describe_loc and
+// describe_thread variants return whether a stack trace logically follows
+// the description; describe_mop and describe_mutex return 1 on success.
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_mop(void *report, uptr idx, int first, char *out,
+                        uptr outlen);
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_loc(void *report, uptr idx, char *out, uptr outlen);
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_mutex(void *report, uptr idx, char *out, uptr outlen);
+SANITIZER_INTERFACE_ATTRIBUTE
+int __tsan_describe_thread(void *report, uptr idx, char *out, uptr outlen);
+
 // Returns the type of the pointer (heap, stack, global, ...) and if possible
 // also the starting address (e.g. of a heap allocation) and size.
 SANITIZER_INTERFACE_ATTRIBUTE
diff --git a/compiler-rt/test/tsan/debug_describe.cpp b/compiler-rt/test/tsan/debug_describe.cpp
new file mode 100644
index 0000000000000..0ce3a84811518
--- /dev/null
+++ b/compiler-rt/test/tsan/debug_describe.cpp
@@ -0,0 +1,114 @@
+// RUN: %clangxx_tsan -O1 %s -o %t
+// RUN: %deflake %run %t 2>&1 | FileCheck %s
+
+#include <pthread.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "test.h"
+
+extern "C" {
+void __tsan_on_report(void *report);
+int __tsan_get_report_data(void *report, const char **description, int *count,
+                           int *stack_count, int *mop_count, int *loc_count,
+                           int *mutex_count, int *thread_count,
+                           int *unique_tid_count, void **sleep_trace,
+                           unsigned long trace_size);
+int __tsan_describe_mop(void *report, unsigned long idx, int first, char *out,
+                        unsigned long outlen);
+int __tsan_describe_loc(void *report, unsigned long idx, char *out,
+                        unsigned long outlen);
+int __tsan_describe_thread(void *report, unsigned long idx, char *out,
+                           unsigned long outlen);
+}
+
+long my_global;
+
+void *Thread(void *a) {
+  barrier_wait(&barrier);
+  my_global = 42;
+  return NULL;
+}
+
+int main() {
+  barrier_init(&barrier, 2);
+  pthread_t t;
+  pthread_create(&t, 0, Thread, 0);
+  my_global = 41;
+  barrier_wait(&barrier);
+  pthread_join(t, 0);
+  fprintf(stderr, "Done.\n");
+}
+
+// Required for dyld macOS 12.0+
+#if (__APPLE__)
+__attribute__((weak))
+#endif
+__attribute__((disable_sanitizer_instrumentation))
+extern "C" void
+__tsan_on_report(void *report) {
+  const char *description;
+  int count, stack_count, mop_count, loc_count, mutex_count, thread_count,
+      unique_tid_count;
+  void *sleep_trace[16] = {0};
+  __tsan_get_report_data(report, &description, &count, &stack_count, &mop_count,
+                         &loc_count, &mutex_count, &thread_count,
+                         &unique_tid_count, sleep_trace, 16);
+
+  char buf[256];
+
+  // Two mops: idx 0 with first=1 ("Write"), idx 1 with first=0 ("Previous
+  // write"). Output starts with two spaces of indent.
+  __tsan_describe_mop(report, 0, /*first=*/1, buf, sizeof(buf));
+  fprintf(stderr, "mop0: [%s]\n", buf);
+  // CHECK: mop0: [{{ +}}Write of size 8 at 0x{{[0-9a-f]+}} by thread T1]
+
+  __tsan_describe_mop(report, 1, /*first=*/0, buf, sizeof(buf));
+  fprintf(stderr, "mop1: [%s]\n", buf);
+  // CHECK: mop1: [{{ +}}Previous write of size 8 at 0x{{[0-9a-f]+}} by main thread]
+
+  // Location is a global; describe_loc returns 0 (no stack follows).
+  if (loc_count > 0) {
+    int has_stack = __tsan_describe_loc(report, 0, buf, sizeof(buf));
+    fprintf(stderr, "loc0 has_stack=%d\n", has_stack);
+    fprintf(stderr, "loc0: [%s]\n", buf);
+    // CHECK: loc0 has_stack=0
+    // CHECK: loc0: [{{.*}}global{{.*}}my_global{{.*}}]
+  }
+
+  // thread[0] = spawned thread, has_stack=1; thread[1] = main, has_stack=0.
+  int t0_has_stack = __tsan_describe_thread(report, 0, buf, sizeof(buf));
+  fprintf(stderr, "thread0 has_stack=%d\n", t0_has_stack);
+  fprintf(stderr, "thread0: [%s]\n", buf);
+  // CHECK: thread0 has_stack=1
+  // CHECK: thread0: [{{.*}}Thread T1{{.*}}created by main thread{{.*}}at:]
+
+  int t1_has_stack = __tsan_describe_thread(report, 1, buf, sizeof(buf));
+  fprintf(stderr, "thread1 has_stack=%d\n", t1_has_stack);
+  // The main thread is described but no stack follows (parent is itself
+  // kInvalidTid in TSan; the function still returns 1 since it isn't a
+  // GCD worker). We pin only that the description was written.
+  fprintf(stderr, "thread1: [%s]\n", buf);
+  // CHECK: thread1 has_stack={{[01]}}
+  // CHECK: thread1: [{{.*}}Thread T0{{.*}}]
+
+  // Truncation: outlen = 8 leaves room for 7 chars of description + null.
+  // Description starts with "  Write" so we expect strlen == 7.
+  char tiny[8];
+  memset(tiny, 'X', sizeof(tiny));
+  __tsan_describe_mop(report, 0, /*first=*/1, tiny, sizeof(tiny));
+  fprintf(stderr, "tiny strlen=%zu\n", strlen(tiny));
+  // CHECK: tiny strlen=7
+
+  // outlen = 0 must not write anything (sentinel preserved).
+  char sentinel[4] = {'A', 'B', 'C', 'D'};
+  __tsan_describe_mop(report, 0, /*first=*/1, sentinel, 0);
+  fprintf(stderr, "sentinel: %c%c%c%c\n", sentinel[0], sentinel[1], sentinel[2],
+          sentinel[3]);
+  // CHECK: sentinel: ABCD
+}
+
+// CHECK: Done.
+// CHECK: ThreadSanitizer: reported 1 warnings

>From e7a036e752f09c253b3f54b76a54096901ef6d88 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 2 Jun 2026 18:54:55 -0700
Subject: [PATCH 5/7] format

---
 .../include/sanitizer/tsan_interface.h        |  3 ++-
 compiler-rt/lib/tsan/rtl/tsan_debugging.cpp   | 21 ++++++++++---------
 compiler-rt/lib/tsan/rtl/tsan_interface.h     |  8 +++----
 compiler-rt/test/tsan/debug_describe.cpp      |  3 +--
 4 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/compiler-rt/include/sanitizer/tsan_interface.h b/compiler-rt/include/sanitizer/tsan_interface.h
index b2fcdfae11127..aa47d30659d2c 100644
--- a/compiler-rt/include/sanitizer/tsan_interface.h
+++ b/compiler-rt/include/sanitizer/tsan_interface.h
@@ -310,7 +310,8 @@ int SANITIZER_CDECL __tsan_get_report_unique_tid(void *report,
 void *SANITIZER_CDECL __tsan_get_current_report();
 
 /// Writes a single-line, uncolored description of a memory operation in the
-/// report into <c>out</c> (e.g. <c>"  Write of size 4 at 0x... by thread T1"</c>).
+/// report into <c>out</c> (e.g. <c>"  Write of size 4 at 0x... by thread
+/// T1"</c>).
 ///
 /// The output is null-terminated and truncated to fit <c>outlen</c> bytes
 /// (including the terminator).
diff --git a/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp b/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
index ee700a2afbc8c..b33b56b63af0e 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_debugging.cpp
@@ -191,8 +191,9 @@ int __tsan_get_report_unique_tid(void *report, uptr idx, int *tid) {
 namespace {
 // Copy `s` (null-terminated) to `out`, truncating to `outlen-1` bytes plus
 // a null terminator. No-op when outlen is zero.
-void CopyDescription(const InternalScopedString &s, char *out, uptr outlen) {
-  if (!out || !outlen) return;
+void CopyDescription(const InternalScopedString& s, char* out, uptr outlen) {
+  if (!out || !outlen)
+    return;
   uptr n = Min(s.length(), outlen - 1);
   internal_memcpy(out, s.data(), n);
   out[n] = '\0';
@@ -200,9 +201,9 @@ void CopyDescription(const InternalScopedString &s, char *out, uptr outlen) {
 }  // namespace
 
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_mop(void *report, uptr idx, int first, char *out,
+int __tsan_describe_mop(void* report, uptr idx, int first, char* out,
                         uptr outlen) {
-  const ReportDesc *rep = (ReportDesc *)report;
+  const ReportDesc* rep = (ReportDesc*)report;
   CHECK_LT(idx, rep->mops.Size());
   InternalScopedString s;
   DescribeMop(rep->mops[idx], first != 0, s);
@@ -211,8 +212,8 @@ int __tsan_describe_mop(void *report, uptr idx, int first, char *out,
 }
 
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_loc(void *report, uptr idx, char *out, uptr outlen) {
-  const ReportDesc *rep = (ReportDesc *)report;
+int __tsan_describe_loc(void* report, uptr idx, char* out, uptr outlen) {
+  const ReportDesc* rep = (ReportDesc*)report;
   CHECK_LT(idx, rep->locs.Size());
   InternalScopedString s;
   bool stack_follows = DescribeLocation(rep->locs[idx], s);
@@ -221,8 +222,8 @@ int __tsan_describe_loc(void *report, uptr idx, char *out, uptr outlen) {
 }
 
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_mutex(void *report, uptr idx, char *out, uptr outlen) {
-  const ReportDesc *rep = (ReportDesc *)report;
+int __tsan_describe_mutex(void* report, uptr idx, char* out, uptr outlen) {
+  const ReportDesc* rep = (ReportDesc*)report;
   CHECK_LT(idx, rep->mutexes.Size());
   InternalScopedString s;
   DescribeMutex(rep->mutexes[idx], s);
@@ -231,8 +232,8 @@ int __tsan_describe_mutex(void *report, uptr idx, char *out, uptr outlen) {
 }
 
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_thread(void *report, uptr idx, char *out, uptr outlen) {
-  const ReportDesc *rep = (ReportDesc *)report;
+int __tsan_describe_thread(void* report, uptr idx, char* out, uptr outlen) {
+  const ReportDesc* rep = (ReportDesc*)report;
   CHECK_LT(idx, rep->threads.Size());
   InternalScopedString s;
   bool stack_follows = DescribeThread(rep->threads[idx], s);
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interface.h b/compiler-rt/lib/tsan/rtl/tsan_interface.h
index b759fc1d10db6..313162947e62e 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interface.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_interface.h
@@ -188,14 +188,14 @@ int __tsan_get_report_unique_tid(void *report, uptr idx, int *tid);
 // describe_thread variants return whether a stack trace logically follows
 // the description; describe_mop and describe_mutex return 1 on success.
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_mop(void *report, uptr idx, int first, char *out,
+int __tsan_describe_mop(void* report, uptr idx, int first, char* out,
                         uptr outlen);
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_loc(void *report, uptr idx, char *out, uptr outlen);
+int __tsan_describe_loc(void* report, uptr idx, char* out, uptr outlen);
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_mutex(void *report, uptr idx, char *out, uptr outlen);
+int __tsan_describe_mutex(void* report, uptr idx, char* out, uptr outlen);
 SANITIZER_INTERFACE_ATTRIBUTE
-int __tsan_describe_thread(void *report, uptr idx, char *out, uptr outlen);
+int __tsan_describe_thread(void* report, uptr idx, char* out, uptr outlen);
 
 // Returns the type of the pointer (heap, stack, global, ...) and if possible
 // also the starting address (e.g. of a heap allocation) and size.
diff --git a/compiler-rt/test/tsan/debug_describe.cpp b/compiler-rt/test/tsan/debug_describe.cpp
index 0ce3a84811518..3ab64b3c9fd01 100644
--- a/compiler-rt/test/tsan/debug_describe.cpp
+++ b/compiler-rt/test/tsan/debug_describe.cpp
@@ -46,8 +46,7 @@ int main() {
 #if (__APPLE__)
 __attribute__((weak))
 #endif
-__attribute__((disable_sanitizer_instrumentation))
-extern "C" void
+__attribute__((disable_sanitizer_instrumentation)) extern "C" void
 __tsan_on_report(void *report) {
   const char *description;
   int count, stack_count, mop_count, loc_count, mutex_count, thread_count,

>From fc5f97c57779c72d4ebbd8c62af9f4b7207c05df Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 2 Jun 2026 18:55:53 -0700
Subject: [PATCH 6/7] Remove changes to asan_descriptions.cpp

---
 compiler-rt/lib/asan/asan_descriptions.cpp | 32 +++++++---------------
 1 file changed, 10 insertions(+), 22 deletions(-)

diff --git a/compiler-rt/lib/asan/asan_descriptions.cpp b/compiler-rt/lib/asan/asan_descriptions.cpp
index 98d7b44085fce..551b819a4c436 100644
--- a/compiler-rt/lib/asan/asan_descriptions.cpp
+++ b/compiler-rt/lib/asan/asan_descriptions.cpp
@@ -36,25 +36,6 @@ AsanThreadIdAndName::AsanThreadIdAndName(u32 tid)
   asanThreadRegistry().CheckLocked();
 }
 
-// Writes a color-free, single-line description of a heap allocation or
-// deallocation stack, e.g. "freed by thread T1 here:".
-static void DescribeAllocStack(const char* verb, AsanThreadContext* thread,
-                               InternalScopedString* s) {
-  s->AppendF("%s by thread %s here:", verb,
-             AsanThreadIdAndName(thread).c_str());
-}
-
-// Prints the description produced by DescribeAllocStack (decorated and followed
-// by a newline) and then the given stack trace.
-static void PrintAllocStack(const char* verb, AsanThreadContext* thread,
-                            const StackTrace& stack) {
-  Decorator d;
-  InternalScopedString s;
-  DescribeAllocStack(verb, thread, &s);
-  Printf("%s%s%s\n", d.Allocation(), s.data(), d.Default());
-  stack.Print();
-}
-
 // Prints this thread and, if flags()->print_full_thread_history, its ancestors
 void DescribeThread(AsanThreadContext *context) {
   while (true) {
@@ -443,14 +424,21 @@ void HeapAddressDescription::Print() const {
   AsanThreadContext *alloc_thread = GetThreadContextByTidLocked(alloc_tid);
   StackTrace alloc_stack = GetStackTraceFromId(alloc_stack_id);
 
+  Decorator d;
   AsanThreadContext *free_thread = nullptr;
   if (free_tid != kInvalidTid) {
     free_thread = GetThreadContextByTidLocked(free_tid);
-    PrintAllocStack("freed", free_thread, GetStackTraceFromId(free_stack_id));
-    PrintAllocStack("previously allocated", alloc_thread, alloc_stack);
+    Printf("%sfreed by thread %s here:%s\n", d.Allocation(),
+           AsanThreadIdAndName(free_thread).c_str(), d.Default());
+    StackTrace free_stack = GetStackTraceFromId(free_stack_id);
+    free_stack.Print();
+    Printf("%spreviously allocated by thread %s here:%s\n", d.Allocation(),
+           AsanThreadIdAndName(alloc_thread).c_str(), d.Default());
   } else {
-    PrintAllocStack("allocated", alloc_thread, alloc_stack);
+    Printf("%sallocated by thread %s here:%s\n", d.Allocation(),
+           AsanThreadIdAndName(alloc_thread).c_str(), d.Default());
   }
+  alloc_stack.Print();
   DescribeThread(GetCurrentThread());
   if (free_thread) DescribeThread(free_thread);
   DescribeThread(alloc_thread);

>From 8587d24539dc40204c77b36c7d96bccf580ed403 Mon Sep 17 00:00:00 2001
From: Andrew Haberlandt <ahaberlandt at apple.com>
Date: Tue, 2 Jun 2026 19:12:08 -0700
Subject: [PATCH 7/7] fix

---
 .../lib/tsan/tests/unit/tsan_report_test.cpp  | 49 ++++++++++++++++---
 1 file changed, 42 insertions(+), 7 deletions(-)

diff --git a/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp b/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp
index 2150f16261859..3178a59504e9a 100644
--- a/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp
+++ b/compiler-rt/lib/tsan/tests/unit/tsan_report_test.cpp
@@ -20,9 +20,19 @@
 #include "tsan_report.h"
 
 #include "gtest/gtest.h"
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_libc.h"
 
 namespace __tsan {
 
+// Format a pointer using the same `%p` formatter the Describe* helpers use,
+// so the expected strings here track whatever zero-padding the platform's
+// printf applies (e.g. glibc and Darwin both pad to 12 hex digits while
+// some libcs print minimal width).
+static void FormatPtr(char *out, uptr size, uptr addr) {
+  __sanitizer::internal_snprintf(out, size, "%p", (void *)addr);
+}
+
 TEST(Report, DescribeMop) {
   ReportMop mop;
   mop.tid = 1;
@@ -32,21 +42,34 @@ TEST(Report, DescribeMop) {
   mop.atomic = false;
   mop.external_tag = kExternalTagNone;
 
+  char addr_str[32];
+  FormatPtr(addr_str, sizeof(addr_str), mop.addr);
+  char expected[128];
+
   InternalScopedString s;
   DescribeMop(&mop, /*first=*/true, s);
-  EXPECT_STREQ("  Write of size 4 at 0x1234 by thread T1", s.data());
+  __sanitizer::internal_snprintf(expected, sizeof(expected),
+                                 "  Write of size 4 at %s by thread T1",
+                                 addr_str);
+  EXPECT_STREQ(expected, s.data());
 
   s.clear();
   mop.write = false;
   DescribeMop(&mop, /*first=*/false, s);
-  EXPECT_STREQ("  Previous read of size 4 at 0x1234 by thread T1", s.data());
+  __sanitizer::internal_snprintf(
+      expected, sizeof(expected),
+      "  Previous read of size 4 at %s by thread T1", addr_str);
+  EXPECT_STREQ(expected, s.data());
 
   s.clear();
   mop.write = true;
   mop.atomic = true;
   mop.tid = kMainTid;
   DescribeMop(&mop, /*first=*/true, s);
-  EXPECT_STREQ("  Atomic write of size 4 at 0x1234 by main thread", s.data());
+  __sanitizer::internal_snprintf(
+      expected, sizeof(expected),
+      "  Atomic write of size 4 at %s by main thread", addr_str);
+  EXPECT_STREQ(expected, s.data());
 }
 
 TEST(Report, DescribeLocationStackAndTls) {
@@ -73,12 +96,18 @@ TEST(Report, DescribeLocationHeap) {
   loc.heap_chunk_size = 16;
   loc.external_tag = kExternalTagNone;
 
+  char addr_str[32];
+  FormatPtr(addr_str, sizeof(addr_str), loc.heap_chunk_start);
+  char expected[128];
+  __sanitizer::internal_snprintf(
+      expected, sizeof(expected),
+      "  Location is heap block of size 16 at %s allocated by thread T3:",
+      addr_str);
+
   InternalScopedString s;
   // Heap locations are followed by the allocation stack.
   EXPECT_TRUE(DescribeLocation(&loc, s));
-  EXPECT_STREQ(
-      "  Location is heap block of size 16 at 0x4000 allocated by thread T3:",
-      s.data());
+  EXPECT_STREQ(expected, s.data());
 }
 
 TEST(Report, DescribeLocationFD) {
@@ -105,9 +134,15 @@ TEST(Report, DescribeMutex) {
   rm.id = 5;
   rm.addr = 0xdead;
 
+  char addr_str[32];
+  FormatPtr(addr_str, sizeof(addr_str), rm.addr);
+  char expected[128];
+  __sanitizer::internal_snprintf(expected, sizeof(expected),
+                                 "  Mutex M5 (%s) created at:", addr_str);
+
   InternalScopedString s;
   DescribeMutex(&rm, s);
-  EXPECT_STREQ("  Mutex M5 (0xdead) created at:", s.data());
+  EXPECT_STREQ(expected, s.data());
 }
 
 TEST(Report, DescribeThread) {



More information about the llvm-commits mailing list