[tsan] Provide API for libraries for race detection on custom objects

This patch allows a non-instrumented library to call into TSan runtime, and tell us about "readonly" and "modifying" accesses to an arbitrary "object" and provide the caller and tag (type of object).  This allows TSan to detect violations of API threading contracts where "read-only" methods can be called simulatenously from multiple threads, while modifying methods must be exclusive.

Differential Revision: https://reviews.llvm.org/D28836

llvm-svn: 293885
This commit is contained in:
Kuba Mracek 2017-02-02 13:17:05 +00:00
parent 5c88271528
commit aa78ad5fea
12 changed files with 289 additions and 12 deletions

View File

@ -25,6 +25,7 @@ append_list_if(COMPILER_RT_HAS_WGLOBAL_CONSTRUCTORS_FLAG -Wglobal-constructors
set(TSAN_SOURCES
rtl/tsan_clock.cc
rtl/tsan_debugging.cc
rtl/tsan_external.cc
rtl/tsan_fd.cc
rtl/tsan_flags.cc
rtl/tsan_ignoreset.cc

View File

@ -24,6 +24,7 @@ static const char *ReportTypeDescription(ReportType typ) {
if (typ == ReportTypeVptrRace) return "data-race-vptr";
if (typ == ReportTypeUseAfterFree) return "heap-use-after-free";
if (typ == ReportTypeVptrUseAfterFree) return "heap-use-after-free-vptr";
if (typ == ReportTypeExternalRace) return "external-race";
if (typ == ReportTypeThreadLeak) return "thread-leak";
if (typ == ReportTypeMutexDestroyLocked) return "locked-mutex-destroy";
if (typ == ReportTypeMutexDoubleLock) return "mutex-double-lock";

View File

@ -149,7 +149,8 @@ class RegionAlloc;
// Descriptor of user's memory block.
struct MBlock {
u64 siz;
u64 siz : 48;
u64 tag : 16;
u32 stk;
u16 tid;
};

View File

@ -0,0 +1,78 @@
//===-- tsan_external.cc --------------------------------------------------===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
//
// This file is a part of ThreadSanitizer (TSan), a race detector.
//
//===----------------------------------------------------------------------===//
#include "tsan_rtl.h"
namespace __tsan {
#define CALLERPC ((uptr)__builtin_return_address(0))
const uptr kMaxTag = 128; // Limited to 65,536, since MBlock only stores tags
// as 16-bit values, see tsan_defs.h.
const char *registered_tags[kMaxTag];
static atomic_uint32_t used_tags{1}; // Tag 0 means "no tag". NOLINT
const char *GetObjectTypeFromTag(uptr tag) {
if (tag == 0) return nullptr;
// Invalid/corrupted tag? Better return NULL and let the caller deal with it.
if (tag >= atomic_load(&used_tags, memory_order_relaxed)) return nullptr;
return registered_tags[tag];
}
extern "C" {
SANITIZER_INTERFACE_ATTRIBUTE
void *__tsan_external_register_tag(const char *object_type) {
uptr new_tag = atomic_fetch_add(&used_tags, 1, memory_order_relaxed);
CHECK_LT(new_tag, kMaxTag);
registered_tags[new_tag] = internal_strdup(object_type);
return (void *)new_tag;
}
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_assign_tag(void *addr, void *tag) {
CHECK_LT(tag, atomic_load(&used_tags, memory_order_relaxed));
Allocator *a = allocator();
MBlock *b = nullptr;
if (a->PointerIsMine((void *)addr)) {
void *block_begin = a->GetBlockBegin((void *)addr);
if (block_begin) b = ctx->metamap.GetBlock((uptr)block_begin);
}
if (b) {
b->tag = (uptr)tag;
}
}
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_read(void *addr, void *caller_pc, void *tag) {
CHECK_LT(tag, atomic_load(&used_tags, memory_order_relaxed));
ThreadState *thr = cur_thread();
thr->external_tag = (uptr)tag;
FuncEntry(thr, (uptr)caller_pc);
MemoryRead(thr, CALLERPC, (uptr)addr, kSizeLog8);
FuncExit(thr);
thr->external_tag = 0;
}
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_write(void *addr, void *caller_pc, void *tag) {
CHECK_LT(tag, atomic_load(&used_tags, memory_order_relaxed));
ThreadState *thr = cur_thread();
thr->external_tag = (uptr)tag;
FuncEntry(thr, (uptr)caller_pc);
MemoryWrite(thr, CALLERPC, (uptr)addr, kSizeLog8);
FuncExit(thr);
thr->external_tag = 0;
}
} // extern "C"
} // namespace __tsan

View File

@ -78,6 +78,15 @@ SANITIZER_INTERFACE_ATTRIBUTE void __tsan_func_exit();
SANITIZER_INTERFACE_ATTRIBUTE void __tsan_ignore_thread_begin();
SANITIZER_INTERFACE_ATTRIBUTE void __tsan_ignore_thread_end();
SANITIZER_INTERFACE_ATTRIBUTE
void *__tsan_external_register_tag(const char *object_type);
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_assign_tag(void *addr, void *tag);
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_read(void *addr, void *caller_pc, void *tag);
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_external_write(void *addr, void *caller_pc, void *tag);
SANITIZER_INTERFACE_ATTRIBUTE
void __tsan_read_range(void *addr, unsigned long size); // NOLINT
SANITIZER_INTERFACE_ATTRIBUTE

View File

@ -90,6 +90,8 @@ static const char *ReportTypeString(ReportType typ) {
return "heap-use-after-free";
if (typ == ReportTypeVptrUseAfterFree)
return "heap-use-after-free (virtual call vs free)";
if (typ == ReportTypeExternalRace)
return "race on a library object";
if (typ == ReportTypeThreadLeak)
return "thread leak";
if (typ == ReportTypeMutexDestroyLocked)
@ -152,14 +154,25 @@ static const char *MopDesc(bool first, bool write, bool atomic) {
: (write ? "Previous write" : "Previous read"));
}
static const char *ExternalMopDesc(bool first, bool write) {
return first ? (write ? "Mutating" : "Read-only")
: (write ? "Previous mutating" : "Previous read-only");
}
static void PrintMop(const ReportMop *mop, bool first) {
Decorator d;
char thrbuf[kThreadBufSize];
Printf("%s", d.Access());
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));
const char *object_type = GetObjectTypeFromTag(mop->external_tag);
if (!object_type) {
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));
} else {
Printf(" %s access of object %s at %p by %s",
ExternalMopDesc(first, mop->write), object_type,
(void *)mop->addr, thread_name(thrbuf, mop->tid));
}
PrintMutexSet(mop->mset);
Printf(":\n");
Printf("%s", d.EndAccess());
@ -183,9 +196,16 @@ static void PrintLocation(const ReportLocation *loc) {
global.module_offset);
} else if (loc->type == ReportLocationHeap) {
char thrbuf[kThreadBufSize];
Printf(" Location is heap block of size %zu at %p allocated by %s:\n",
loc->heap_chunk_size, loc->heap_chunk_start,
thread_name(thrbuf, loc->tid));
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, loc->heap_chunk_start,
thread_name(thrbuf, loc->tid));
} else {
Printf(" Location is %s object of size %zu at %p allocated by %s:\n",
object_type, loc->heap_chunk_size, 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));

View File

@ -24,6 +24,7 @@ enum ReportType {
ReportTypeVptrRace,
ReportTypeUseAfterFree,
ReportTypeVptrUseAfterFree,
ReportTypeExternalRace,
ReportTypeThreadLeak,
ReportTypeMutexDestroyLocked,
ReportTypeMutexDoubleLock,
@ -56,6 +57,7 @@ struct ReportMop {
int size;
bool write;
bool atomic;
uptr external_tag;
Vector<ReportMopMutex> mset;
ReportStack *stack;
@ -75,6 +77,7 @@ struct ReportLocation {
DataInfo global;
uptr heap_chunk_start;
uptr heap_chunk_size;
uptr external_tag;
int tid;
int fd;
bool suppressable;

View File

@ -410,6 +410,7 @@ struct ThreadState {
bool is_dead;
bool is_freeing;
bool is_vptr_access;
uptr external_tag;
const uptr stk_addr;
const uptr stk_size;
const uptr tls_addr;
@ -564,7 +565,7 @@ class ScopedReport {
explicit ScopedReport(ReportType typ);
~ScopedReport();
void AddMemoryAccess(uptr addr, Shadow s, StackTrace stack,
void AddMemoryAccess(uptr addr, uptr external_tag, Shadow s, StackTrace stack,
const MutexSet *mset);
void AddStack(StackTrace stack, bool suppressable = false);
void AddThread(const ThreadContext *tctx, bool suppressable = false);
@ -640,6 +641,8 @@ bool IsFiredSuppression(Context *ctx, ReportType type, StackTrace trace);
bool IsExpectedReport(uptr addr, uptr size);
void PrintMatchedBenignRaces();
const char *GetObjectTypeFromTag(uptr tag);
#if defined(TSAN_DEBUG_OUTPUT) && TSAN_DEBUG_OUTPUT >= 1
# define DPrintf Printf
#else

View File

@ -164,8 +164,8 @@ void ScopedReport::AddStack(StackTrace stack, bool suppressable) {
(*rs)->suppressable = suppressable;
}
void ScopedReport::AddMemoryAccess(uptr addr, Shadow s, StackTrace stack,
const MutexSet *mset) {
void ScopedReport::AddMemoryAccess(uptr addr, uptr external_tag, Shadow s,
StackTrace stack, const MutexSet *mset) {
void *mem = internal_alloc(MBlockReportMop, sizeof(ReportMop));
ReportMop *mop = new(mem) ReportMop;
rep_->mops.PushBack(mop);
@ -175,6 +175,7 @@ void ScopedReport::AddMemoryAccess(uptr addr, Shadow s, StackTrace stack,
mop->write = s.IsWrite();
mop->atomic = s.IsAtomic();
mop->stack = SymbolizeStack(stack);
mop->external_tag = external_tag;
if (mop->stack)
mop->stack->suppressable = true;
for (uptr i = 0; i < mset->Size(); i++) {
@ -337,6 +338,7 @@ void ScopedReport::AddLocation(uptr addr, uptr size) {
ReportLocation *loc = ReportLocation::New(ReportLocationHeap);
loc->heap_chunk_start = (uptr)allocator()->GetBlockBegin((void *)addr);
loc->heap_chunk_size = b->siz;
loc->external_tag = b->tag;
loc->tid = tctx ? tctx->tid : b->tid;
loc->stack = SymbolizeStackId(b->stk);
rep_->locs.PushBack(loc);
@ -623,6 +625,8 @@ void ReportRace(ThreadState *thr) {
typ = ReportTypeVptrRace;
else if (freed)
typ = ReportTypeUseAfterFree;
else if (thr->external_tag > 0)
typ = ReportTypeExternalRace;
if (IsFiredSuppression(ctx, typ, addr))
return;
@ -651,7 +655,8 @@ void ReportRace(ThreadState *thr) {
ScopedReport rep(typ);
for (uptr i = 0; i < kMop; i++) {
Shadow s(thr->racy_state[i]);
rep.AddMemoryAccess(addr, s, traces[i], i == 0 ? &thr->mset : mset2);
rep.AddMemoryAccess(addr, thr->external_tag, s, traces[i],
i == 0 ? &thr->mset : mset2);
}
for (uptr i = 0; i < kMop; i++) {

View File

@ -74,6 +74,8 @@ static const char *conv(ReportType typ) {
return kSuppressionRace;
else if (typ == ReportTypeVptrUseAfterFree)
return kSuppressionRace;
else if (typ == ReportTypeExternalRace)
return kSuppressionRace;
else if (typ == ReportTypeThreadLeak)
return kSuppressionThread;
else if (typ == ReportTypeMutexDestroyLocked)

View File

@ -64,6 +64,7 @@ void MetaMap::AllocBlock(ThreadState *thr, uptr pc, uptr p, uptr sz) {
u32 idx = block_alloc_.Alloc(&thr->proc()->block_cache);
MBlock *b = block_alloc_.Map(idx);
b->siz = sz;
b->tag = 0;
b->tid = thr->tid;
b->stk = CurrentStackId(thr, pc);
u32 *meta = MemToMeta(p);

View File

@ -0,0 +1,153 @@
// RUN: %clangxx_tsan %s -o %t-lib-instrumented.dylib -shared -DSHARED_LIB
// RUN: %clangxx_tsan %s -o %t-lib-noninstrumented.dylib -shared -DSHARED_LIB -fno-sanitize=thread
// RUN: %clangxx_tsan %s -o %t-lib-noninstrumented-callbacks.dylib -shared -DSHARED_LIB -fno-sanitize=thread -DUSE_TSAN_CALLBACKS
// RUN: %clangxx_tsan %s %t-lib-instrumented.dylib -o %t-lib-instrumented
// RUN: %clangxx_tsan %s %t-lib-noninstrumented.dylib -o %t-lib-noninstrumented
// RUN: %clangxx_tsan %s %t-lib-noninstrumented-callbacks.dylib -o %t-lib-noninstrumented-callbacks
// RUN: %deflake %run %t-lib-instrumented 2>&1 \
// RUN: | FileCheck %s --check-prefix=CHECK --check-prefix=TEST1
// RUN: %run %t-lib-noninstrumented 2>&1 \
// RUN: | FileCheck %s --check-prefix=CHECK --check-prefix=TEST2
// RUN: %deflake %run %t-lib-noninstrumented-callbacks 2>&1 \
// RUN: | FileCheck %s --check-prefix=CHECK --check-prefix=TEST3
#include <thread>
#include <dlfcn.h>
#include <pthread.h>
#include <stdio.h>
struct MyObject;
typedef MyObject *MyObjectRef;
extern "C" {
void InitializeLibrary();
MyObject *ObjectCreate();
long ObjectRead(MyObject *);
void ObjectWrite(MyObject *, long);
void ObjectWriteAnother(MyObject *, long);
}
#if defined(SHARED_LIB)
struct MyObject {
long _val;
long _another;
};
#if defined(USE_TSAN_CALLBACKS)
static void *tag;
void *(*callback_register_tag)(const char *object_type);
void *(*callback_assign_tag)(void *addr, void *tag);
void (*callback_read)(void *addr, void *caller_pc, void *tag);
void (*callback_write)(void *addr, void *caller_pc, void *tag);
#endif
void InitializeLibrary() {
#if defined(USE_TSAN_CALLBACKS)
callback_register_tag = (decltype(callback_register_tag))dlsym(RTLD_DEFAULT, "__tsan_external_register_tag");
callback_assign_tag = (decltype(callback_assign_tag))dlsym(RTLD_DEFAULT, "__tsan_external_assign_tag");
callback_read = (decltype(callback_read))dlsym(RTLD_DEFAULT, "__tsan_external_read");
callback_write = (decltype(callback_write))dlsym(RTLD_DEFAULT, "__tsan_external_write");
tag = callback_register_tag("MyLibrary::MyObject");
#endif
}
MyObject *ObjectCreate() {
MyObject *ref = (MyObject *)malloc(sizeof(MyObject));
#if defined(USE_TSAN_CALLBACKS)
callback_assign_tag(ref, tag);
#endif
return ref;
}
long ObjectRead(MyObject *ref) {
#if defined(USE_TSAN_CALLBACKS)
callback_read(ref, __builtin_return_address(0), tag);
#endif
return ref->_val;
}
void ObjectWrite(MyObject *ref, long val) {
#if defined(USE_TSAN_CALLBACKS)
callback_write(ref, __builtin_return_address(0), tag);
#endif
ref->_val = val;
}
void ObjectWriteAnother(MyObject *ref, long val) {
#if defined(USE_TSAN_CALLBACKS)
callback_write(ref, __builtin_return_address(0), tag);
#endif
ref->_another = val;
}
#else // defined(SHARED_LIB)
int main(int argc, char *argv[]) {
InitializeLibrary();
{
MyObjectRef ref = ObjectCreate();
std::thread t1([ref]{ ObjectRead(ref); });
std::thread t2([ref]{ ObjectRead(ref); });
t1.join();
t2.join();
}
// CHECK-NOT: WARNING: ThreadSanitizer
fprintf(stderr, "RR test done\n");
// CHECK: RR test done
{
MyObjectRef ref = ObjectCreate();
std::thread t1([ref]{ ObjectRead(ref); });
std::thread t2([ref]{ ObjectWrite(ref, 66); });
t1.join();
t2.join();
}
// TEST1: WARNING: ThreadSanitizer: data race
// TEST1: {{Write|Read}} of size 8 at
// TEST1: Previous {{write|read}} of size 8 at
// TEST1: Location is heap block of size 16 at
// TEST2-NOT: WARNING: ThreadSanitizer
// TEST3: WARNING: ThreadSanitizer: race on a library object
// TEST3: {{Mutating|read-only}} access of object MyLibrary::MyObject at
// TEST3: {{ObjectWrite|ObjectRead}}
// TEST3: Previous {{mutating|read-only}} access of object MyLibrary::MyObject at
// TEST3: {{ObjectWrite|ObjectRead}}
// TEST3: Location is MyLibrary::MyObject object of size 16 at
// TEST3: {{ObjectCreate}}
fprintf(stderr, "RW test done\n");
// CHECK: RW test done
{
MyObjectRef ref = ObjectCreate();
std::thread t1([ref]{ ObjectWrite(ref, 76); });
std::thread t2([ref]{ ObjectWriteAnother(ref, 77); });
t1.join();
t2.join();
}
// TEST1-NOT: WARNING: ThreadSanitizer: data race
// TEST2-NOT: WARNING: ThreadSanitizer
// TEST3: WARNING: ThreadSanitizer: race on a library object
// TEST3: Mutating access of object MyLibrary::MyObject at
// TEST3: {{ObjectWrite|ObjectWriteAnother}}
// TEST3: Previous mutating access of object MyLibrary::MyObject at
// TEST3: {{ObjectWrite|ObjectWriteAnother}}
// TEST3: Location is MyLibrary::MyObject object of size 16 at
// TEST3: {{ObjectCreate}}
fprintf(stderr, "WW test done\n");
// CHECK: WW test done
}
#endif // defined(SHARED_LIB)