forked from OSchip/llvm-project
[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:
parent
5c88271528
commit
aa78ad5fea
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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++) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue