[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