mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
SILGen: Add experimental TSan instrumentation for inout accesses. (#7736)
Add SILGen instrumentation to treat inout accesses as Thread Sanitizer writes. The goal is to catch races on inout accesses even when there is a not an llvm-level read/write to a particular address. Ultimately this will enable TSan to, for example, report racy writes to distinct stored properties of a common struct as a data race. This instrumentation is off by default. It can be enabled with the 'enable-experimental-tsan-inout-instrumentation' frontend flag. The high-level approach is to add a SIL-level builtin that represents a call to a TSan routine in compiler-rt. Then, when emitting an address for an LValue as part of an inout expression, we call this builtin for each path component that represents an LValue. I've added an 'isRValue()' method to PathComponent that tracks whether a component represents an RValue or an LValue. Right the only PathComponent that sometimes returns 'true' is ValueComponent(). For now, we're instrumenting only InoutExprs, but in the future it probably makes sense to instrument all LValue accesses. In this patch I've added a 'TSanKind' parameter to SILGenFunction::emitAddressOfLValue() and its helpers to limit instrumentation to inout accesses. I envision that this parameter will eventually go away.
This commit is contained in:
@@ -162,6 +162,10 @@ namespace swift {
|
|||||||
/// \brief Enable experimental property behavior feature.
|
/// \brief Enable experimental property behavior feature.
|
||||||
bool EnableExperimentalPropertyBehaviors = false;
|
bool EnableExperimentalPropertyBehaviors = false;
|
||||||
|
|
||||||
|
/// \brief Staging flag for treating inout parameters as Thread Sanitizer
|
||||||
|
/// accesses.
|
||||||
|
bool EnableTSANInoutInstrumentation = false;
|
||||||
|
|
||||||
/// \brief Staging flag for class resilience, which we do not want to enable
|
/// \brief Staging flag for class resilience, which we do not want to enable
|
||||||
/// fully until more code is in place, to allow the standard library to be
|
/// fully until more code is in place, to allow the standard library to be
|
||||||
/// tested with value type resilience only.
|
/// tested with value type resilience only.
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ def disable_availability_checking : Flag<["-"],
|
|||||||
"disable-availability-checking">,
|
"disable-availability-checking">,
|
||||||
HelpText<"Disable checking for potentially unavailable APIs">;
|
HelpText<"Disable checking for potentially unavailable APIs">;
|
||||||
|
|
||||||
|
def enable_experimental_tsan_inout_instrumentation : Flag<["-"],
|
||||||
|
"enable-experimental-tsan-inout-instrumentation">,
|
||||||
|
HelpText<"Enable treatment of inout parameters as Thread Sanitizer accesses">;
|
||||||
|
|
||||||
def enable_infer_import_as_member :
|
def enable_infer_import_as_member :
|
||||||
Flag<["-"], "enable-infer-import-as-member">,
|
Flag<["-"], "enable-infer-import-as-member">,
|
||||||
HelpText<"Infer when a global could be imported as a member">;
|
HelpText<"Infer when a global could be imported as a member">;
|
||||||
|
|||||||
@@ -847,6 +847,10 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
|
|||||||
|
|
||||||
Opts.DisableAvailabilityChecking |=
|
Opts.DisableAvailabilityChecking |=
|
||||||
Args.hasArg(OPT_disable_availability_checking);
|
Args.hasArg(OPT_disable_availability_checking);
|
||||||
|
|
||||||
|
Opts.EnableTSANInoutInstrumentation |=
|
||||||
|
Args.hasArg(OPT_enable_experimental_tsan_inout_instrumentation);
|
||||||
|
|
||||||
if (FrontendOpts.InputKind == InputFileKind::IFK_SIL)
|
if (FrontendOpts.InputKind == InputFileKind::IFK_SIL)
|
||||||
Opts.DisableAvailabilityChecking = true;
|
Opts.DisableAvailabilityChecking = true;
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ public:
|
|||||||
/// base value.
|
/// base value.
|
||||||
virtual AccessKind getBaseAccessKind(SILGenFunction &SGF,
|
virtual AccessKind getBaseAccessKind(SILGenFunction &SGF,
|
||||||
AccessKind accessKind) const = 0;
|
AccessKind accessKind) const = 0;
|
||||||
|
|
||||||
|
virtual bool isRValue() const { return false; }
|
||||||
|
|
||||||
/// Returns the logical type-as-rvalue of the value addressed by the
|
/// Returns the logical type-as-rvalue of the value addressed by the
|
||||||
/// component. This is always an object type, never an address.
|
/// component. This is always an object type, never an address.
|
||||||
SILType getTypeOfRValue() const { return TypeData.TypeOfRValue; }
|
SILType getTypeOfRValue() const { return TypeData.TypeOfRValue; }
|
||||||
|
|||||||
@@ -1987,7 +1987,8 @@ static void beginInOutFormalAccesses(SILGenFunction &SGF,
|
|||||||
LValue &inoutArg = inoutNext->first;
|
LValue &inoutArg = inoutNext->first;
|
||||||
SILLocation loc = inoutNext->second;
|
SILLocation loc = inoutNext->second;
|
||||||
ManagedValue address = SGF.emitAddressOfLValue(loc, std::move(inoutArg),
|
ManagedValue address = SGF.emitAddressOfLValue(loc, std::move(inoutArg),
|
||||||
AccessKind::ReadWrite);
|
AccessKind::ReadWrite,
|
||||||
|
TSanKind::InoutAccess);
|
||||||
siteArg = address;
|
siteArg = address;
|
||||||
emittedInoutArgs.push_back({address.getValue(), loc});
|
emittedInoutArgs.push_back({address.getValue(), loc});
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,16 @@ enum class CaptureEmission {
|
|||||||
PartialApplication,
|
PartialApplication,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Parameter to \c SILGenFunction::emitAddressOfLValue that indicates
|
||||||
|
/// what kind of instrumentation should be emitted when compiling under
|
||||||
|
/// Thread Sanitizer.
|
||||||
|
enum class TSanKind : bool {
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// Instrument the LValue access as an inout access.
|
||||||
|
InoutAccess
|
||||||
|
};
|
||||||
|
|
||||||
/// Represents an LValue opened for mutating access.
|
/// Represents an LValue opened for mutating access.
|
||||||
///
|
///
|
||||||
/// This is used by LogicalPathComponent::getMaterialized() and
|
/// This is used by LogicalPathComponent::getMaterialized() and
|
||||||
@@ -1238,7 +1248,9 @@ public:
|
|||||||
void emitCopyLValueInto(SILLocation loc, LValue &&src,
|
void emitCopyLValueInto(SILLocation loc, LValue &&src,
|
||||||
Initialization *dest);
|
Initialization *dest);
|
||||||
ManagedValue emitAddressOfLValue(SILLocation loc, LValue &&src,
|
ManagedValue emitAddressOfLValue(SILLocation loc, LValue &&src,
|
||||||
AccessKind accessKind);
|
AccessKind accessKind,
|
||||||
|
TSanKind tsanKind = TSanKind::None);
|
||||||
|
|
||||||
RValue emitLoadOfLValue(SILLocation loc, LValue &&src, SGFContext C,
|
RValue emitLoadOfLValue(SILLocation loc, LValue &&src, SGFContext C,
|
||||||
bool isGuaranteedValid = false);
|
bool isGuaranteedValid = false);
|
||||||
|
|
||||||
|
|||||||
@@ -454,10 +454,14 @@ namespace {
|
|||||||
/// A physical path component which returns a literal address.
|
/// A physical path component which returns a literal address.
|
||||||
class ValueComponent : public PhysicalPathComponent {
|
class ValueComponent : public PhysicalPathComponent {
|
||||||
ManagedValue Value;
|
ManagedValue Value;
|
||||||
|
bool IsRValue;
|
||||||
public:
|
public:
|
||||||
ValueComponent(ManagedValue value, LValueTypeData typeData) :
|
ValueComponent(ManagedValue value, LValueTypeData typeData,
|
||||||
|
bool isRValue = false) :
|
||||||
PhysicalPathComponent(typeData, ValueKind),
|
PhysicalPathComponent(typeData, ValueKind),
|
||||||
Value(value) {
|
Value(value),
|
||||||
|
IsRValue(isRValue) {
|
||||||
|
assert(IsRValue || value.getType().isAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
ManagedValue offset(SILGenFunction &SGF, SILLocation loc, ManagedValue base,
|
ManagedValue offset(SILGenFunction &SGF, SILLocation loc, ManagedValue base,
|
||||||
@@ -466,6 +470,10 @@ namespace {
|
|||||||
return Value;
|
return Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isRValue() const override {
|
||||||
|
return IsRValue;
|
||||||
|
}
|
||||||
|
|
||||||
void print(raw_ostream &OS) const override {
|
void print(raw_ostream &OS) const override {
|
||||||
OS << "ValueComponent()\n";
|
OS << "ValueComponent()\n";
|
||||||
}
|
}
|
||||||
@@ -1351,7 +1359,7 @@ LValue LValue::forValue(ManagedValue value,
|
|||||||
value.getValue());
|
value.getValue());
|
||||||
|
|
||||||
LValue lv;
|
LValue lv;
|
||||||
lv.add<ValueComponent>(value, typeData);
|
lv.add<ValueComponent>(value, typeData, /*isRValue=*/true);
|
||||||
return lv;
|
return lv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1514,7 +1522,7 @@ LValue SILGenLValue::visitRec(Expr *e, AccessKind accessKind) {
|
|||||||
CanType formalType = getSubstFormalRValueType(e);
|
CanType formalType = getSubstFormalRValueType(e);
|
||||||
auto typeData = getValueTypeData(formalType, rv.getValue());
|
auto typeData = getValueTypeData(formalType, rv.getValue());
|
||||||
LValue lv;
|
LValue lv;
|
||||||
lv.add<ValueComponent>(rv, typeData);
|
lv.add<ValueComponent>(rv, typeData, /*isRValue=*/true);
|
||||||
return lv;
|
return lv;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1974,7 +1982,7 @@ LValue SILGenFunction::emitPropertyLValue(SILLocation loc, ManagedValue base,
|
|||||||
base.getValue());
|
base.getValue());
|
||||||
|
|
||||||
// Refer to 'self' as the base of the lvalue.
|
// Refer to 'self' as the base of the lvalue.
|
||||||
lv.add<ValueComponent>(base, baseTypeData);
|
lv.add<ValueComponent>(base, baseTypeData, /*isRValue=*/!base.isLValue());
|
||||||
|
|
||||||
auto substFormalType = ivar->getInterfaceType().subst(subMap)
|
auto substFormalType = ivar->getInterfaceType().subst(subMap)
|
||||||
->getCanonicalType();
|
->getCanonicalType();
|
||||||
@@ -2396,13 +2404,23 @@ SILValue SILGenFunction::emitConversionFromSemanticValue(SILLocation loc,
|
|||||||
llvm_unreachable("unexpected storage type that differs from type-of-rvalue");
|
llvm_unreachable("unexpected storage type that differs from type-of-rvalue");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void emitTsanInoutAccess(SILGenFunction &SGF, SILLocation loc,
|
||||||
|
ManagedValue address) {
|
||||||
|
assert(address.getType().isAddress());
|
||||||
|
SILValue accessFnArgs[] = {address.getValue()};
|
||||||
|
|
||||||
|
SGF.B.createBuiltin(loc, SGF.getASTContext().getIdentifier("tsanInoutAccess"),
|
||||||
|
SGF.SGM.Types.getEmptyTupleType(), {}, accessFnArgs);
|
||||||
|
}
|
||||||
|
|
||||||
/// Produce a physical address that corresponds to the given l-value
|
/// Produce a physical address that corresponds to the given l-value
|
||||||
/// component.
|
/// component.
|
||||||
static ManagedValue drillIntoComponent(SILGenFunction &SGF,
|
static ManagedValue drillIntoComponent(SILGenFunction &SGF,
|
||||||
SILLocation loc,
|
SILLocation loc,
|
||||||
PathComponent &&component,
|
PathComponent &&component,
|
||||||
ManagedValue base,
|
ManagedValue base,
|
||||||
AccessKind accessKind) {
|
AccessKind accessKind,
|
||||||
|
TSanKind tsanKind) {
|
||||||
ManagedValue addr;
|
ManagedValue addr;
|
||||||
if (component.isPhysical()) {
|
if (component.isPhysical()) {
|
||||||
addr = std::move(component.asPhysical()).offset(SGF, loc, base, accessKind);
|
addr = std::move(component.asPhysical()).offset(SGF, loc, base, accessKind);
|
||||||
@@ -2411,16 +2429,23 @@ static ManagedValue drillIntoComponent(SILGenFunction &SGF,
|
|||||||
addr = std::move(lcomponent).getMaterialized(SGF, loc, base, accessKind);
|
addr = std::move(lcomponent).getMaterialized(SGF, loc, base, accessKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SGF.getASTContext().LangOpts.EnableTSANInoutInstrumentation &&
|
||||||
|
tsanKind == TSanKind::InoutAccess && !component.isRValue()) {
|
||||||
|
emitTsanInoutAccess(SGF, loc, addr);
|
||||||
|
}
|
||||||
|
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the last component of the given lvalue and derive a base
|
/// Find the last component of the given lvalue and derive a base
|
||||||
/// location for it.
|
/// location for it.
|
||||||
static PathComponent &&drillToLastComponent(SILGenFunction &SGF,
|
static PathComponent &&
|
||||||
SILLocation loc,
|
drillToLastComponent(SILGenFunction &SGF,
|
||||||
LValue &&lv,
|
SILLocation loc,
|
||||||
ManagedValue &addr,
|
LValue &&lv,
|
||||||
AccessKind accessKind) {
|
ManagedValue &addr,
|
||||||
|
AccessKind accessKind,
|
||||||
|
TSanKind tsanKind = TSanKind::None) {
|
||||||
assert(lv.begin() != lv.end() &&
|
assert(lv.begin() != lv.end() &&
|
||||||
"lvalue must have at least one component");
|
"lvalue must have at least one component");
|
||||||
|
|
||||||
@@ -2432,7 +2457,8 @@ static PathComponent &&drillToLastComponent(SILGenFunction &SGF,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto i = lv.begin(), e = lv.end() - 1; i != e; ++i) {
|
for (auto i = lv.begin(), e = lv.end() - 1; i != e; ++i) {
|
||||||
addr = drillIntoComponent(SGF, loc, std::move(**i), addr, accessKind);
|
addr = drillIntoComponent(SGF, loc, std::move(**i), addr, accessKind,
|
||||||
|
tsanKind);
|
||||||
accessKind = pathAccessKinds.pop_back_val();
|
accessKind = pathAccessKinds.pop_back_val();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2464,11 +2490,15 @@ RValue SILGenFunction::emitLoadOfLValue(SILLocation loc, LValue &&src,
|
|||||||
|
|
||||||
ManagedValue SILGenFunction::emitAddressOfLValue(SILLocation loc,
|
ManagedValue SILGenFunction::emitAddressOfLValue(SILLocation loc,
|
||||||
LValue &&src,
|
LValue &&src,
|
||||||
AccessKind accessKind) {
|
AccessKind accessKind,
|
||||||
|
TSanKind tsanKind) {
|
||||||
ManagedValue addr;
|
ManagedValue addr;
|
||||||
PathComponent &&component =
|
PathComponent &&component =
|
||||||
drillToLastComponent(*this, loc, std::move(src), addr, accessKind);
|
drillToLastComponent(*this, loc, std::move(src), addr, accessKind,
|
||||||
addr = drillIntoComponent(*this, loc, std::move(component), addr, accessKind);
|
tsanKind);
|
||||||
|
|
||||||
|
addr = drillIntoComponent(*this, loc, std::move(component), addr, accessKind,
|
||||||
|
tsanKind);
|
||||||
assert(addr.getType().isAddress() &&
|
assert(addr.getType().isAddress() &&
|
||||||
"resolving lvalue did not give an address");
|
"resolving lvalue did not give an address");
|
||||||
return ManagedValue::forLValue(addr.getValue());
|
return ManagedValue::forLValue(addr.getValue());
|
||||||
|
|||||||
@@ -594,6 +594,21 @@ void ElementUseCollector::collectContainerUses(AllocBoxInst *ABI) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true when the instruction represents added instrumentation for
|
||||||
|
/// run-time sanitizers.
|
||||||
|
static bool isSanitizerInstrumentation(SILInstruction *Instruction,
|
||||||
|
ASTContext &Ctx) {
|
||||||
|
auto *BI = dyn_cast<BuiltinInst>(Instruction);
|
||||||
|
if (!BI)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Identifier Name = BI->getName();
|
||||||
|
if (Name == Ctx.getIdentifier("tsanInoutAccess"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void ElementUseCollector::collectUses(SILValue Pointer, unsigned BaseEltNo) {
|
void ElementUseCollector::collectUses(SILValue Pointer, unsigned BaseEltNo) {
|
||||||
assert(Pointer->getType().isAddress() &&
|
assert(Pointer->getType().isAddress() &&
|
||||||
"Walked through the pointer to the value?");
|
"Walked through the pointer to the value?");
|
||||||
@@ -832,6 +847,11 @@ void ElementUseCollector::collectUses(SILValue Pointer, unsigned BaseEltNo) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanitizer instrumentation is not user visible, so it should not
|
||||||
|
// count as a use and must not affect compile-time diagnostics.
|
||||||
|
if (isSanitizerInstrumentation(User, Module.getASTContext()))
|
||||||
|
continue;
|
||||||
|
|
||||||
// Otherwise, the use is something complicated, it escapes.
|
// Otherwise, the use is something complicated, it escapes.
|
||||||
addElementUses(BaseEltNo, PointeeType, User, DIUseKind::Escape);
|
addElementUses(BaseEltNo, PointeeType, User, DIUseKind::Escape);
|
||||||
}
|
}
|
||||||
|
|||||||
66
test/SILGen/tsan_instrumentation.swift
Normal file
66
test/SILGen/tsan_instrumentation.swift
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// RUN: %target-swift-frontend -Xllvm -new-mangling-for-tests -sanitize=thread -enable-experimental-tsan-inout-instrumentation -emit-silgen %s | %FileCheck %s
|
||||||
|
// REQUIRES: tsan_runtime
|
||||||
|
// XFAIL: linux
|
||||||
|
|
||||||
|
func takesInout(_ p: inout Int) { }
|
||||||
|
func takesInout(_ p: inout MyStruct) { }
|
||||||
|
|
||||||
|
|
||||||
|
struct MyStruct {
|
||||||
|
var storedProperty: Int = 77
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyClass {
|
||||||
|
var storedProperty: Int = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
var gStruct = MyStruct()
|
||||||
|
var gClass = MyClass()
|
||||||
|
|
||||||
|
// CHECK-LABEL: sil hidden @_T020tsan_instrumentation17inoutGlobalStructyyF : $@convention(thin) () -> () {
|
||||||
|
// CHECK: [[GLOBAL_ADDR:%.*]] = global_addr @_T020tsan_instrumentation7gStructAA02MyC0Vv : $*MyStruct
|
||||||
|
// CHECK: [[TAKES_INOUT_FUNC:%.*]] = function_ref @_T020tsan_instrumentation10takesInoutyAA8MyStructVzF : $@convention(thin) (@inout MyStruct) -> ()
|
||||||
|
// CHECK: {{%.*}} = builtin "tsanInoutAccess"([[GLOBAL_ADDR]] : $*MyStruct) : $()
|
||||||
|
// CHECK: {{%.*}} = apply [[TAKES_INOUT_FUNC]]([[GLOBAL_ADDR]]) : $@convention(thin) (@inout MyStruct) -> ()
|
||||||
|
func inoutGlobalStruct() {
|
||||||
|
takesInout(&gStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CHECK-LABEL: sil hidden @_T020tsan_instrumentation31inoutGlobalStructStoredPropertyyyF : $@convention(thin) () -> () {
|
||||||
|
// CHECK: [[GLOBAL_ADDR:%.*]] = global_addr @_T020tsan_instrumentation7gStructAA02MyC0Vv : $*MyStruct
|
||||||
|
// CHECK: [[TAKES_INOUT_FUNC:%.*]] = function_ref @_T020tsan_instrumentation10takesInoutySizF : $@convention(thin) (@inout Int) -> ()
|
||||||
|
// CHECK: {{%.*}} = builtin "tsanInoutAccess"([[GLOBAL_ADDR]] : $*MyStruct) : $()
|
||||||
|
// CHECK: [[ELEMENT_ADDR:%.*]] = struct_element_addr [[GLOBAL_ADDR]] : $*MyStruct, #MyStruct.storedProperty
|
||||||
|
// CHECK: {{%.*}} = builtin "tsanInoutAccess"([[ELEMENT_ADDR]] : $*Int) : $()
|
||||||
|
// CHECK: {{%.*}} = apply [[TAKES_INOUT_FUNC]]([[ELEMENT_ADDR]]) : $@convention(thin) (@inout Int) -> ()
|
||||||
|
func inoutGlobalStructStoredProperty() {
|
||||||
|
// This should generate two TSan inout instrumentations; one for the address
|
||||||
|
// of the global and one for the address of the struct stored property.
|
||||||
|
takesInout(&gStruct.storedProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHECK-LABEL: sil hidden @_T020tsan_instrumentation30inoutGlobalClassStoredPropertyyyF : $@convention(thin) () -> () {
|
||||||
|
// CHECK: [[GLOBAL_ADDR:%.*]] = global_addr @_T020tsan_instrumentation6gClassAA02MyC0Cv : $*MyClass
|
||||||
|
// CHECK: [[TAKES_INOUT_FUNC:%.*]] = function_ref @_T020tsan_instrumentation10takesInoutySizF : $@convention(thin) (@inout Int) -> ()
|
||||||
|
// CHECK: [[LOADED_CLASS:%.*]] = load [copy] [[GLOBAL_ADDR]] : $*MyClass
|
||||||
|
// CHECK: [[VALUE_BUFFER:%.*]] = alloc_stack $Builtin.UnsafeValueBuffer
|
||||||
|
// CHECK: [[TEMPORARY:%.*]] = alloc_stack $Int
|
||||||
|
// CHECK: [[BORROWED_CLASS:%.*]] = begin_borrow [[LOADED_CLASS]] : $MyClass
|
||||||
|
// CHECK: [[TEMPORARY_RAW:%.*]] = address_to_pointer [[TEMPORARY]] : $*Int to $Builtin.RawPointer
|
||||||
|
// CHECK: [[MATERIALIZE_FOR_SET:%.*]] = class_method [[BORROWED_CLASS]] : $MyClass, #MyClass.storedProperty!materializeForSet.1 : (MyClass) -> (Builtin.RawPointer, inout Builtin.UnsafeValueBuffer) -> (Builtin.RawPointer, Builtin.RawPointer?), $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed MyClass) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>)
|
||||||
|
// CHECK: {{%.*}} = builtin "tsanInoutAccess"([[VALUE_BUFFER]] : $*Builtin.UnsafeValueBuffer) : $()
|
||||||
|
// CHECK: [[MATERIALIZE_FOR_SET_TUPLE:%.*]] = apply [[MATERIALIZE_FOR_SET]]([[TEMPORARY_RAW]], [[VALUE_BUFFER]], [[BORROWED_CLASS]]) : $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed MyClass) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>)
|
||||||
|
// CHECK: [[TEMPORARY_BUFFER:%.*]] = tuple_extract [[MATERIALIZE_FOR_SET_TUPLE]] : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 0
|
||||||
|
// CHECK: [[OPTIONAL_CALLBACK:%.*]] = tuple_extract [[MATERIALIZE_FOR_SET_TUPLE]] : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 1
|
||||||
|
// CHECK: [[BUFFER_ADDRESS:%.*]] = pointer_to_address [[TEMPORARY_BUFFER]] : $Builtin.RawPointer to [strict] $*Int
|
||||||
|
// CHECK: [[BUFFER_ADDRESS_DEPENDENCE:%.*]] = mark_dependence [[BUFFER_ADDRESS]] : $*Int on [[LOADED_CLASS]] : $MyClass
|
||||||
|
// CHECK: end_borrow [[BORROWED_CLASS]] from [[LOADED_CLASS]] : $MyClass, $MyClass
|
||||||
|
// CHECK: {{%.*}} builtin "tsanInoutAccess"([[BUFFER_ADDRESS_DEPENDENCE]] : $*Int) : $()
|
||||||
|
// CHECK: {{%.*}} apply [[TAKES_INOUT_FUNC]]([[BUFFER_ADDRESS_DEPENDENCE]]) : $@convention(thin) (@inout Int) -> ()
|
||||||
|
func inoutGlobalClassStoredProperty() {
|
||||||
|
// This generates two TSan inout instrumentations. One for the value
|
||||||
|
// buffer that is passed inout to materializeForSet and one for the
|
||||||
|
// temporary buffer passed to takesInout().
|
||||||
|
takesInout(&gClass.storedProperty)
|
||||||
|
}
|
||||||
62
test/Sanitizers/Inputs/tsan-uninstrumented.swift
Normal file
62
test/Sanitizers/Inputs/tsan-uninstrumented.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// This is uninstrumented code used for testing calls into uninstrumented
|
||||||
|
// modules.
|
||||||
|
|
||||||
|
public struct UninstrumentedStruct {
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public func read() -> Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public mutating func mutate() { }
|
||||||
|
|
||||||
|
public var storedProperty1: Int = 7
|
||||||
|
public var storedProperty2: Int = 22
|
||||||
|
|
||||||
|
public subscript(index: Int) -> Int {
|
||||||
|
get { return 0 }
|
||||||
|
set(newValue) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var storedClass: UninstrumentedClass? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UninstrumentedClass {
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public func read() -> Int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func mutate() { }
|
||||||
|
|
||||||
|
public var storedProperty1: Int = 7
|
||||||
|
public var storedProperty2: Int = 22
|
||||||
|
|
||||||
|
public subscript(index: Int) -> Int {
|
||||||
|
get { return 0 }
|
||||||
|
set(newValue) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var storedStructProperty: UninstrumentedStruct = UninstrumentedStruct()
|
||||||
|
|
||||||
|
public var computedStructProperty: UninstrumentedStruct {
|
||||||
|
get { return UninstrumentedStruct() }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func uninstrumentedTakesInout(_ i: inout Int) { }
|
||||||
|
|
||||||
|
public var storedGlobalInUninstrumentedModule1: Int = 7
|
||||||
|
public var storedGlobalInUninstrumentedModule2: Int = 88
|
||||||
|
|
||||||
|
public var computedGlobalInUninstrumentedModule1: Int {
|
||||||
|
get { return 0 }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public var computedGlobalInUninstrumentedModule2: Int {
|
||||||
|
get { return 0 }
|
||||||
|
set { }
|
||||||
|
}
|
||||||
298
test/Sanitizers/tsan-inout.swift
Normal file
298
test/Sanitizers/tsan-inout.swift
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// RUN: %target-build-swift %S/Inputs/tsan-uninstrumented.swift -module-name TSanUninstrumented -emit-module -emit-module-path %T/TSanUninstrumented.swiftmodule -parse-as-library
|
||||||
|
// RUN: %target-build-swift %S/Inputs/tsan-uninstrumented.swift -c -module-name TSanUninstrumented -parse-as-library -o %T/TSanUninstrumented.o
|
||||||
|
// RUN: %target-swiftc_driver -Xfrontend -enable-experimental-tsan-inout-instrumentation %s %T/TSanUninstrumented.o -I%T -L%T -g -sanitize=thread -o %t_tsan-binary
|
||||||
|
// RUN: not env TSAN_OPTIONS=abort_on_error=0 %target-run %t_tsan-binary 2>&1 | %FileCheck %s
|
||||||
|
// REQUIRES: executable_test
|
||||||
|
// REQUIRES: objc_interop
|
||||||
|
// REQUIRES: CPU=x86_64
|
||||||
|
// REQUIRES: tsan_runtime
|
||||||
|
// XFAIL: linux
|
||||||
|
|
||||||
|
// Test ThreadSanitizer execution end-to-end when calling
|
||||||
|
// an uninstrumented module with inout parameters
|
||||||
|
|
||||||
|
import Darwin
|
||||||
|
import TSanUninstrumented
|
||||||
|
|
||||||
|
// Globals to allow closures passed to pthread_create() to be thin.
|
||||||
|
var gInThread1: () -> () = { }
|
||||||
|
var gInThread2: () -> () = { }
|
||||||
|
|
||||||
|
// Spawn two threads, run the the two passed in closures simultaneously, and
|
||||||
|
// join them.
|
||||||
|
func testRace(name: String, thread inThread1: @escaping () -> (), thread inThread2: @escaping () -> ()) {
|
||||||
|
var thread1: pthread_t?
|
||||||
|
var thread2: pthread_t?
|
||||||
|
print("Running \(name)")
|
||||||
|
fflush(stdout)
|
||||||
|
|
||||||
|
// Store these in globals so the closure passed to pthread_create
|
||||||
|
// can be turned into a C function pointer.
|
||||||
|
gInThread1 = inThread1
|
||||||
|
gInThread2 = inThread2
|
||||||
|
pthread_create(&thread1, nil, { _ in
|
||||||
|
gInThread1()
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
pthread_create(&thread2, nil, { _ in
|
||||||
|
gInThread2()
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
_ = pthread_join(thread1!, nil)
|
||||||
|
_ = pthread_join(thread2!, nil)
|
||||||
|
|
||||||
|
// TSan reports go to stderr
|
||||||
|
fflush(stderr)
|
||||||
|
print("Done \(name)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class InstrumentedClass {
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public var storedProperty1: Int = 7
|
||||||
|
public var storedProperty2: Int = 22
|
||||||
|
|
||||||
|
public var storedStructProperty: UninstrumentedStruct = UninstrumentedStruct()
|
||||||
|
|
||||||
|
private var _backingStoredProperty: Int = 7
|
||||||
|
public var computedPropertyBackedByStoredProperty: Int {
|
||||||
|
get {
|
||||||
|
return _backingStoredProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
set(newVal) {
|
||||||
|
_backingStoredProperty = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tests for accesses to globals
|
||||||
|
// We use different globals for each test to avoid suppressions due
|
||||||
|
// to TSan's issue uniquing logic.
|
||||||
|
|
||||||
|
var globalForGlobalStructMutatingMethod = UninstrumentedStruct()
|
||||||
|
testRace(name: "GlobalStructMutatingMethod",
|
||||||
|
thread: { _ = globalForGlobalStructMutatingMethod.read() },
|
||||||
|
thread: { globalForGlobalStructMutatingMethod.mutate() } )
|
||||||
|
// CHECK-LABEL: Running GlobalStructMutatingMethod
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
var globalForGlobalStructDifferentStoredPropertiesInout = UninstrumentedStruct()
|
||||||
|
testRace(name: "GlobalStructDifferentStoredPropertiesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructDifferentStoredPropertiesInout.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructDifferentStoredPropertiesInout.storedProperty2) } )
|
||||||
|
// CHECK-LABEL: Running GlobalStructDifferentStoredPropertiesInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
var globalForGlobalStructSameStoredPropertyInout = UninstrumentedStruct()
|
||||||
|
testRace(name: "GlobalStructSameStoredPropertyInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructSameStoredPropertyInout.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructSameStoredPropertyInout.storedProperty1) } )
|
||||||
|
// CHECK-LABEL: Running GlobalStructSameStoredPropertyInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
|
||||||
|
var globalForGlobalStructSubscriptDifferentIndexesInout = UninstrumentedStruct()
|
||||||
|
testRace(name: "GlobalStructSubscriptDifferentIndexesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructSubscriptDifferentIndexesInout[0]) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalStructSubscriptDifferentIndexesInout[1]) } )
|
||||||
|
// CHECK-LABEL: Running GlobalStructSubscriptDifferentIndexes
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
|
||||||
|
var globalForGlobalStructSubscriptDifferentIndexesGetSet = UninstrumentedStruct()
|
||||||
|
testRace(name: "GlobalStructSubscriptDifferentIndexesGetSet",
|
||||||
|
thread: { _ = globalForGlobalStructSubscriptDifferentIndexesGetSet[0] },
|
||||||
|
thread: { globalForGlobalStructSubscriptDifferentIndexesGetSet[1] = 12 } )
|
||||||
|
// CHECK-LABEL: Running GlobalStructSubscriptDifferentIndexesGetSet
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
var globalForGlobalClassGeneralMethods = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalClassGeneralMethods",
|
||||||
|
thread: { _ = globalForGlobalClassGeneralMethods.read() },
|
||||||
|
thread: { globalForGlobalClassGeneralMethods.mutate() } )
|
||||||
|
// CHECK-LABEL: Running GlobalClassGeneralMethods
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
var globalForGlobalClassDifferentStoredPropertiesInout = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalClassDifferentStoredPropertiesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassDifferentStoredPropertiesInout.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassDifferentStoredPropertiesInout.storedProperty2) } )
|
||||||
|
// CHECK-LABEL: Running GlobalClassDifferentStoredPropertiesInout
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
var globalForGlobalClassSubscriptDifferentIndexesInout = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalClassSubscriptDifferentIndexesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassSubscriptDifferentIndexesInout[0]) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassSubscriptDifferentIndexesInout[1]) } )
|
||||||
|
// CHECK-LABEL: Running GlobalClassSubscriptDifferentIndexesInout
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
|
||||||
|
var globalForGlobalClassSameStoredPropertyInout = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalClassSameStoredPropertyInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassSameStoredPropertyInout.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalClassSameStoredPropertyInout.storedProperty1) } )
|
||||||
|
// CHECK-LABEL: Running GlobalClassSameStoredPropertyInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
|
||||||
|
// These access a global declared in the TSanUninstrumented module
|
||||||
|
testRace(name: "InoutAccessToStoredGlobalInUninstrumentedModule",
|
||||||
|
thread: { uninstrumentedTakesInout(&storedGlobalInUninstrumentedModule1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&storedGlobalInUninstrumentedModule1) } )
|
||||||
|
// CHECK-LABEL: Running InoutAccessToStoredGlobalInUninstrumentedModule
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
// These access a global declared in the TSanUninstrumented module
|
||||||
|
testRace(name: "ReadAndWriteToStoredGlobalInUninstrumentedModule",
|
||||||
|
thread: { storedGlobalInUninstrumentedModule2 = 7 },
|
||||||
|
thread: { _ = storedGlobalInUninstrumentedModule2 } )
|
||||||
|
// CHECK-LABEL: Running ReadAndWriteToStoredGlobalInUninstrumentedModule
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is global
|
||||||
|
|
||||||
|
// These access a computed global declared in the TSanUninstrumented module
|
||||||
|
testRace(name: "InoutAccessToComputedGlobalInUninstrumentedModule",
|
||||||
|
thread: { uninstrumentedTakesInout(&computedGlobalInUninstrumentedModule1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&computedGlobalInUninstrumentedModule1) } )
|
||||||
|
// CHECK-LABEL: Running InoutAccessToComputedGlobalInUninstrumentedModule
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
// These access a computed global declared in the TSanUninstrumented module
|
||||||
|
testRace(name: "ReadAndWriteToComputedGlobalInUninstrumentedModule",
|
||||||
|
thread: { computedGlobalInUninstrumentedModule2 = 7 },
|
||||||
|
thread: { _ = computedGlobalInUninstrumentedModule2 } )
|
||||||
|
// CHECK-LABEL: Running ReadAndWriteToComputedGlobalInUninstrumentedModule
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Tests for accesses to stored class properties
|
||||||
|
|
||||||
|
var globalForGlobalUninstrumentedClassStoredPropertyMutatingMethod = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalUninstrumentedClassStoredPropertyMutatingMethod",
|
||||||
|
thread: { _ = globalForGlobalUninstrumentedClassStoredPropertyMutatingMethod.storedStructProperty.read() },
|
||||||
|
thread: { globalForGlobalUninstrumentedClassStoredPropertyMutatingMethod.storedStructProperty.mutate() } )
|
||||||
|
// CHECK-LABEL: Running GlobalUninstrumentedClassStoredPropertyMutatingMethod
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
// Note: TSan doesn't see a race above because it doesn't see any load on the
|
||||||
|
// read side because the getter for the class property is not instrumented.
|
||||||
|
|
||||||
|
|
||||||
|
var globalForGlobalUninstrumentedClassStoredPropertyInout = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalUninstrumentedClassStoredPropertyInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalUninstrumentedClassStoredPropertyInout.storedStructProperty.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalUninstrumentedClassStoredPropertyInout.storedStructProperty.storedProperty2) } )
|
||||||
|
// CHECK-LABEL: Running GlobalUninstrumentedClassStoredPropertyInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
|
||||||
|
// Note: TSan sees the race above because the inout instrumentation adds an
|
||||||
|
// ''access'' at the call site to the address returned from materializeForSet
|
||||||
|
|
||||||
|
|
||||||
|
var globalForGlobalUninstrumentedClassComputedPropertyInout = UninstrumentedClass()
|
||||||
|
testRace(name: "GlobalUninstrumentedClassComputedPropertyInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalUninstrumentedClassComputedPropertyInout.computedStructProperty.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&globalForGlobalUninstrumentedClassComputedPropertyInout.computedStructProperty.storedProperty1) } )
|
||||||
|
// CHECK-LABEL: Running GlobalUninstrumentedClassComputedPropertyInout
|
||||||
|
// CHECK-NO: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
// In the above the write in instrumented code is to the value buffer allocated
|
||||||
|
// at the call site so there is no data race if the getter and setters themselves
|
||||||
|
// are synchronized/don't access shared storage. Even with synchronized accessors,
|
||||||
|
// there is still the possibility of a race condition with lost updates with
|
||||||
|
// some interleavings of the calls to the getters and setters -- but no data race.
|
||||||
|
|
||||||
|
var globalForGlobalInstrumentedClassStoredPropertyMutatingMethod = InstrumentedClass()
|
||||||
|
testRace(name: "GlobalInstrumentedClassStoredPropertyMutatingMethod",
|
||||||
|
thread: { _ = globalForGlobalInstrumentedClassStoredPropertyMutatingMethod.storedStructProperty.read() },
|
||||||
|
thread: { globalForGlobalInstrumentedClassStoredPropertyMutatingMethod.storedStructProperty.mutate() } )
|
||||||
|
// CHECK-LABEL: Running GlobalInstrumentedClassStoredPropertyMutatingMethod
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
//
|
||||||
|
// TSan does see this above race because the getter and materializeForSet is instrumented
|
||||||
|
|
||||||
|
var globalForGlobalInstrumentedComputedBackedProperty = InstrumentedClass()
|
||||||
|
testRace(name: "GlobalInstrumentedComputedBackedProperty",
|
||||||
|
thread: { _ = globalForGlobalInstrumentedComputedBackedProperty.computedPropertyBackedByStoredProperty },
|
||||||
|
thread: { globalForGlobalInstrumentedComputedBackedProperty.computedPropertyBackedByStoredProperty = 77 } )
|
||||||
|
// CHECK-LABEL: Running GlobalInstrumentedComputedBackedProperty
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
//
|
||||||
|
// TSan does see this above race because the getter and setter are instrumented
|
||||||
|
// and write to a shared heap location.
|
||||||
|
|
||||||
|
func runLocalTests() {
|
||||||
|
runCapturedLocalStructMutatingMethod()
|
||||||
|
runCapturedLocalStructDifferentStoredPropertiesInout()
|
||||||
|
runCapturedLocalClassGeneralMethods()
|
||||||
|
runCapturedLocalDifferentStoredPropertiesInout()
|
||||||
|
runCapturedLocalSameStoredPropertyInout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCapturedLocalStructMutatingMethod() {
|
||||||
|
var l = UninstrumentedStruct()
|
||||||
|
testRace(name: "CapturedLocalStructMutatingMethod",
|
||||||
|
thread: { _ = l.read() },
|
||||||
|
thread: { l.mutate() } )
|
||||||
|
}
|
||||||
|
// CHECK-LABEL: Running CapturedLocalStructMutatingMethod
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
|
||||||
|
|
||||||
|
func runCapturedLocalStructDifferentStoredPropertiesInout() {
|
||||||
|
var l = UninstrumentedStruct()
|
||||||
|
testRace(name: "CapturedLocalStructDifferentStoredPropertiesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty2) } )
|
||||||
|
}
|
||||||
|
// CHECK-LABEL: Running CapturedLocalStructDifferentStoredPropertiesInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
|
||||||
|
|
||||||
|
func runCapturedLocalClassGeneralMethods() {
|
||||||
|
let l = UninstrumentedClass()
|
||||||
|
testRace(name: "CapturedLocalClassGeneralMethods",
|
||||||
|
thread: { _ = l.read() },
|
||||||
|
thread: { l.mutate() } )
|
||||||
|
}
|
||||||
|
// CHECK-LABEL: Running CapturedLocalClassGeneralMethods
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
|
||||||
|
func runCapturedLocalDifferentStoredPropertiesInout() {
|
||||||
|
let l = UninstrumentedClass()
|
||||||
|
testRace(name: "CapturedLocalClassDifferentStoredPropertiesInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty2) } )
|
||||||
|
}
|
||||||
|
// CHECK-LABEL: Running CapturedLocalClassDifferentStoredPropertiesInout
|
||||||
|
// CHECK-NOT: ThreadSanitizer: data race
|
||||||
|
|
||||||
|
func runCapturedLocalSameStoredPropertyInout() {
|
||||||
|
let l = UninstrumentedClass()
|
||||||
|
testRace(name: "CapturedLocalClassSameStoredPropertyInout",
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty1) },
|
||||||
|
thread: { uninstrumentedTakesInout(&l.storedProperty1) } )
|
||||||
|
}
|
||||||
|
// CHECK-LABEL: Running CapturedLocalClassSameStoredPropertyInout
|
||||||
|
// CHECK: ThreadSanitizer: data race
|
||||||
|
// CHECK: Location is heap block
|
||||||
|
|
||||||
|
runLocalTests()
|
||||||
Reference in New Issue
Block a user