Skip to content

Commit 015b7ed

Browse files
Detect class inited status more correctly in prestub/jitinterface (#104253)
* Fix #104247 by providing a new helper function which means what the old IsClassInited helper meant - AND a very subtle race condition where we need to set the IsClassInited flag bit on dynamic statics BEFORE setting the MethodTable level one
1 parent 2ea6ae5 commit 015b7ed

File tree

8 files changed

+97
-50
lines changed

8 files changed

+97
-50
lines changed

src/coreclr/vm/appdomain.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -672,7 +672,7 @@ void AppDomain::SetNativeDllSearchDirectories(LPCWSTR wszNativeDllSearchDirector
672672
}
673673
}
674674

675-
OBJECTREF* BaseDomain::AllocateObjRefPtrsInLargeTable(int nRequested, DynamicStaticsInfo* pStaticsInfo, MethodTable *pMTToFillWithStaticBoxes)
675+
OBJECTREF* BaseDomain::AllocateObjRefPtrsInLargeTable(int nRequested, DynamicStaticsInfo* pStaticsInfo, MethodTable *pMTToFillWithStaticBoxes, bool isClassInitdeByUpdatingStaticPointer)
676676
{
677677
CONTRACTL
678678
{
@@ -707,7 +707,7 @@ OBJECTREF* BaseDomain::AllocateObjRefPtrsInLargeTable(int nRequested, DynamicSta
707707
if (pStaticsInfo)
708708
{
709709
// race with other threads that might be doing the same concurrent allocation
710-
if (!pStaticsInfo->InterlockedUpdateStaticsPointer(/*isGCPointer*/ true, (TADDR)result))
710+
if (!pStaticsInfo->InterlockedUpdateStaticsPointer(/*isGCPointer*/ true, (TADDR)result, isClassInitdeByUpdatingStaticPointer))
711711
{
712712
// we lost the race, release our handles and use the handles from the
713713
// winning thread
@@ -2930,7 +2930,7 @@ void AppDomain::SetupSharedStatics()
29302930

29312931
FieldDesc * pEmptyStringFD = CoreLibBinder::GetField(FIELD__STRING__EMPTY);
29322932
OBJECTREF* pEmptyStringHandle = (OBJECTREF*)
2933-
((TADDR)g_pStringClass->GetDynamicStaticsInfo()->m_pGCStatics+pEmptyStringFD->GetOffset());
2933+
((TADDR)g_pStringClass->GetDynamicStaticsInfo()->GetGCStaticsPointer()+pEmptyStringFD->GetOffset());
29342934
SetObjectReference( pEmptyStringHandle, StringObject::GetEmptyString());
29352935
}
29362936

src/coreclr/vm/appdomain.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ class BaseDomain
522522
// Statics and reflection info (Types, MemberInfo,..) are stored this way
523523
// If pStaticsInfo != 0, allocation will only take place if GC statics in the DynamicStaticsInfo are NULL (and the allocation
524524
// will be properly serialized)
525-
OBJECTREF *AllocateObjRefPtrsInLargeTable(int nRequested, DynamicStaticsInfo* pStaticsInfo = NULL, MethodTable *pMTToFillWithStaticBoxes = NULL);
525+
OBJECTREF *AllocateObjRefPtrsInLargeTable(int nRequested, DynamicStaticsInfo* pStaticsInfo = NULL, MethodTable *pMTToFillWithStaticBoxes = NULL, bool isClassInitdeByUpdatingStaticPointer = false);
526526

527527
//****************************************************************************************
528528
// Handles

src/coreclr/vm/jitinterface.cpp

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,10 +1210,9 @@ CorInfoHelpFunc CEEInfo::getSharedStaticsHelper(FieldDesc * pField, MethodTable
12101210
{
12111211
STANDARD_VM_CONTRACT;
12121212

1213-
pFieldMT->AttemptToPreinit();
12141213
bool GCStatic = (pField->GetFieldType() == ELEMENT_TYPE_CLASS ||
12151214
pField->GetFieldType() == ELEMENT_TYPE_VALUETYPE);
1216-
bool noCtor = pFieldMT->IsClassInited();
1215+
bool noCtor = pFieldMT->IsClassInitedOrPreinited();
12171216
bool threadStatic = pField->IsThreadStatic();
12181217
bool isInexactMT = pFieldMT->IsSharedByGenericInstantiations();
12191218
bool isCollectible = pFieldMT->Collectible();
@@ -1451,7 +1450,7 @@ void CEEInfo::getFieldInfo (CORINFO_RESOLVED_TOKEN * pResolvedToken,
14511450
}
14521451

14531452
// We are not going through a helper. The constructor has to be triggered explicitly.
1454-
if (!pFieldMT->IsClassInited())
1453+
if (!pFieldMT->IsClassInitedOrPreinited())
14551454
fieldFlags |= CORINFO_FLG_FIELD_INITCLASS;
14561455
}
14571456
else
@@ -1536,10 +1535,9 @@ void CEEInfo::getFieldInfo (CORINFO_RESOLVED_TOKEN * pResolvedToken,
15361535
// Allocate space for the local class if necessary, but don't trigger
15371536
// class construction.
15381537
pFieldMT->EnsureStaticDataAllocated();
1539-
pFieldMT->AttemptToPreinit();
15401538

15411539
// We are not going through a helper. The constructor has to be triggered explicitly.
1542-
if (!pFieldMT->IsClassInited())
1540+
if (!pFieldMT->IsClassInitedOrPreinited())
15431541
fieldFlags |= CORINFO_FLG_FIELD_INITCLASS;
15441542

15451543
GCX_COOP();
@@ -3884,8 +3882,7 @@ CorInfoInitClassResult CEEInfo::initClass(
38843882

38853883
MethodTable *pTypeToInitMT = typeToInitTH.AsMethodTable();
38863884

3887-
pTypeToInitMT->AttemptToPreinit();
3888-
if (pTypeToInitMT->IsClassInited())
3885+
if (pTypeToInitMT->IsClassInitedOrPreinited())
38893886
{
38903887
// If the type is initialized there really is nothing to do.
38913888
result = CORINFO_INITCLASS_INITIALIZED;
@@ -11726,7 +11723,7 @@ bool CEEInfo::getStaticFieldContent(CORINFO_FIELD_HANDLE fieldHnd, uint8_t* buff
1172611723
// class construction.
1172711724
pEnclosingMT->EnsureStaticDataAllocated();
1172811725

11729-
if (!field->IsThreadStatic() && pEnclosingMT->IsClassInited() && IsFdInitOnly(field->GetAttributes()))
11726+
if (!field->IsThreadStatic() && pEnclosingMT->IsClassInitedOrPreinited() && IsFdInitOnly(field->GetAttributes()))
1173011727
{
1173111728
if (field->IsObjRef())
1173211729
{
@@ -11914,7 +11911,7 @@ CORINFO_CLASS_HANDLE CEEJitInfo::getStaticFieldCurrentClass(CORINFO_FIELD_HANDLE
1191411911
VALIDATEOBJECTREF(fieldObj);
1191511912

1191611913
// Check for initialization before looking at the value
11917-
isClassInitialized = !!pEnclosingMT->IsClassInited();
11914+
isClassInitialized = !!pEnclosingMT->IsClassInitedOrPreinited();
1191811915

1191911916
if (fieldObj != NULL)
1192011917
{

src/coreclr/vm/loaderallocator.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,7 +2254,7 @@ PTR_OnStackReplacementManager LoaderAllocator::GetOnStackReplacementManager()
22542254
#endif // FEATURE_ON_STACK_REPLACEMENT
22552255

22562256
#ifndef DACCESS_COMPILE
2257-
void LoaderAllocator::AllocateBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cbMem)
2257+
void LoaderAllocator::AllocateBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cbMem, bool isClassInitedByUpdatingStaticPointer)
22582258
{
22592259
CONTRACTL
22602260
{
@@ -2294,7 +2294,7 @@ void LoaderAllocator::AllocateBytesForStaticVariables(DynamicStaticsInfo* pStati
22942294
WeakInteriorHandleHolder weakHandleHolder = GetAppDomain()->CreateWeakInteriorHandle(ptrArray, &pStaticsInfo->m_pNonGCStatics);
22952295
RegisterHandleForCleanupLocked(weakHandleHolder.GetValue());
22962296
weakHandleHolder.SuppressRelease();
2297-
bool didUpdateStaticsPointer = pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */false, (TADDR)ptrArray->GetDataPtr());
2297+
bool didUpdateStaticsPointer = pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */false, (TADDR)ptrArray->GetDataPtr(), isClassInitedByUpdatingStaticPointer);
22982298
_ASSERTE(didUpdateStaticsPointer);
22992299
}
23002300
}
@@ -2324,11 +2324,11 @@ void LoaderAllocator::AllocateBytesForStaticVariables(DynamicStaticsInfo* pStati
23242324
pbMem = (uint8_t*)ALIGN_UP(pbMem, 8);
23252325
}
23262326
#endif
2327-
pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */false, (TADDR)pbMem);
2327+
pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */false, (TADDR)pbMem, isClassInitedByUpdatingStaticPointer);
23282328
}
23292329
}
23302330

2331-
void LoaderAllocator::AllocateGCHandlesBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cSlots, MethodTable* pMTToFillWithStaticBoxes)
2331+
void LoaderAllocator::AllocateGCHandlesBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cSlots, MethodTable* pMTToFillWithStaticBoxes, bool isClassInitedByUpdatingStaticPointer)
23322332
{
23332333
CONTRACTL
23342334
{
@@ -2374,15 +2374,15 @@ void LoaderAllocator::AllocateGCHandlesBytesForStaticVariables(DynamicStaticsInf
23742374
WeakInteriorHandleHolder weakHandleHolder = GetAppDomain()->CreateWeakInteriorHandle(ptrArray, &pStaticsInfo->m_pGCStatics);
23752375
RegisterHandleForCleanupLocked(weakHandleHolder.GetValue());
23762376
weakHandleHolder.SuppressRelease();
2377-
bool didUpdateStaticsPointer = pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */true, (TADDR)ptrArray->GetDataPtr());
2377+
bool didUpdateStaticsPointer = pStaticsInfo->InterlockedUpdateStaticsPointer(/* isGCPointer */true, (TADDR)ptrArray->GetDataPtr(), isClassInitedByUpdatingStaticPointer);
23782378
_ASSERTE(didUpdateStaticsPointer);
23792379
}
23802380
}
23812381
GCPROTECT_END();
23822382
}
23832383
else
23842384
{
2385-
GetDomain()->AllocateObjRefPtrsInLargeTable(cSlots, pStaticsInfo, pMTToFillWithStaticBoxes);
2385+
GetDomain()->AllocateObjRefPtrsInLargeTable(cSlots, pStaticsInfo, pMTToFillWithStaticBoxes, isClassInitedByUpdatingStaticPointer);
23862386
}
23872387
}
23882388
#endif // !DACCESS_COMPILE

src/coreclr/vm/loaderallocator.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,8 +761,8 @@ class LoaderAllocator
761761
LIMITED_METHOD_CONTRACT;
762762
return m_nGCCount;
763763
}
764-
void AllocateBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cbMem);
765-
void AllocateGCHandlesBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cSlots, MethodTable* pMTWithStaticBoxes);
764+
void AllocateBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cbMem, bool isClassInitedByUpdatingStaticPointer);
765+
void AllocateGCHandlesBytesForStaticVariables(DynamicStaticsInfo* pStaticsInfo, uint32_t cSlots, MethodTable* pMTWithStaticBoxes, bool isClassInitedByUpdatingStaticPointer);
766766

767767
static BOOL Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator);
768768

src/coreclr/vm/methodtable.cpp

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3782,20 +3782,42 @@ void MethodTable::EnsureStaticDataAllocated()
37823782
CONTRACTL_END;
37833783

37843784
PTR_MethodTableAuxiliaryData pAuxiliaryData = GetAuxiliaryDataForWrite();
3785-
if (!pAuxiliaryData->IsStaticDataAllocated() && IsDynamicStatics())
3785+
if (!pAuxiliaryData->IsStaticDataAllocated())
37863786
{
3787-
DynamicStaticsInfo *pDynamicStaticsInfo = GetDynamicStaticsInfo();
3788-
// Allocate space for normal statics if we might have them
3789-
if (pDynamicStaticsInfo->GetNonGCStaticsPointer() == NULL)
3790-
GetLoaderAllocator()->AllocateBytesForStaticVariables(pDynamicStaticsInfo, GetClass()->GetNonGCRegularStaticFieldBytes());
3787+
bool isInitedIfStaticDataAllocated = IsInitedIfStaticDataAllocated();
3788+
if (IsDynamicStatics() && !IsSharedByGenericInstantiations())
3789+
{
3790+
DynamicStaticsInfo *pDynamicStaticsInfo = GetDynamicStaticsInfo();
3791+
// Allocate space for normal statics if we might have them
3792+
if (pDynamicStaticsInfo->GetNonGCStaticsPointer() == NULL)
3793+
GetLoaderAllocator()->AllocateBytesForStaticVariables(pDynamicStaticsInfo, GetClass()->GetNonGCRegularStaticFieldBytes(), isInitedIfStaticDataAllocated);
37913794

3792-
if (pDynamicStaticsInfo->GetGCStaticsPointer() == NULL)
3793-
GetLoaderAllocator()->AllocateGCHandlesBytesForStaticVariables(pDynamicStaticsInfo, GetClass()->GetNumHandleRegularStatics(), this->HasBoxedRegularStatics() ? this : NULL);
3795+
if (pDynamicStaticsInfo->GetGCStaticsPointer() == NULL)
3796+
GetLoaderAllocator()->AllocateGCHandlesBytesForStaticVariables(pDynamicStaticsInfo, GetClass()->GetNumHandleRegularStatics(), this->HasBoxedRegularStatics() ? this : NULL, isInitedIfStaticDataAllocated);
3797+
}
3798+
pAuxiliaryData->SetIsStaticDataAllocated(isInitedIfStaticDataAllocated);
37943799
}
3795-
pAuxiliaryData->SetIsStaticDataAllocated();
37963800
}
37973801

3798-
void MethodTable::AttemptToPreinit()
3802+
bool MethodTable::IsClassInitedOrPreinited()
3803+
{
3804+
CONTRACTL
3805+
{
3806+
THROWS;
3807+
GC_TRIGGERS;
3808+
INJECT_FAULT(COMPlusThrowOM());
3809+
}
3810+
CONTRACTL_END;
3811+
3812+
bool initResult;
3813+
if (GetAuxiliaryData()->IsClassInitedOrPreinitedDecided(&initResult))
3814+
return initResult;
3815+
3816+
EnsureStaticDataAllocated();
3817+
return IsClassInited();
3818+
}
3819+
3820+
bool MethodTable::IsInitedIfStaticDataAllocated()
37993821
{
38003822
CONTRACTL
38013823
{
@@ -3806,33 +3828,32 @@ void MethodTable::AttemptToPreinit()
38063828
CONTRACTL_END;
38073829

38083830
if (IsClassInited())
3809-
return;
3831+
{
3832+
return true;
3833+
}
38103834

38113835
if (HasClassConstructor())
38123836
{
38133837
// If there is a class constructor, then the class cannot be preinitted.
3814-
return;
3838+
return false;
38153839
}
38163840

38173841
if (GetClass()->GetNonGCRegularStaticFieldBytes() == 0 && GetClass()->GetNumHandleRegularStatics() == 0)
38183842
{
3819-
// If there are static fields that are not thread statics, then the class is preinitted.
3820-
SetClassInited();
3821-
return;
3843+
// If there aren't static fields that are not thread statics, then the class is preinitted.
3844+
return true;
38223845
}
38233846

38243847
// At this point, we are looking at a class that has no class constructor, but does have static fields
38253848

38263849
if (IsSharedByGenericInstantiations())
38273850
{
3828-
// If we don't know the exact type, we can't pre-allocate the fields
3829-
return;
3851+
// If we don't know the exact type, we can't pre-init the the fields
3852+
return false;
38303853
}
38313854

38323855
// All this class needs to be initialized is to allocate the memory for the static fields. Do so, and mark the type as initialized
3833-
EnsureStaticDataAllocated();
3834-
SetClassInited();
3835-
return;
3856+
return true;
38363857
}
38373858

38383859
void MethodTable::EnsureTlsIndexAllocated()

src/coreclr/vm/methodtable.h

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ struct MethodTableAuxiliaryData
337337
enum_flag_DependenciesLoaded = 0x0080, // class and all dependencies loaded up to CLASS_LOADED_BUT_NOT_VERIFIED
338338

339339
enum_flag_IsInitError = 0x0100,
340-
enum_flag_IsStaticDataAllocated = 0x0200,
340+
enum_flag_IsStaticDataAllocated = 0x0200, // When this is set, if the class can be marked as initialized without any further code execution it will be.
341341
// unum_unused = 0x0400,
342342
enum_flag_IsTlsIndexAllocated = 0x0800,
343343
enum_flag_MayHaveOpenInterfaceInInterfaceMap = 0x1000,
@@ -447,9 +447,19 @@ struct MethodTableAuxiliaryData
447447

448448
inline BOOL IsClassInited() const
449449
{
450+
LIMITED_METHOD_DAC_CONTRACT;
450451
return VolatileLoad(&m_dwFlags) & enum_flag_Initialized;
451452
}
452453

454+
inline bool IsClassInitedOrPreinitedDecided(bool *initResult) const
455+
{
456+
LIMITED_METHOD_DAC_CONTRACT;
457+
458+
DWORD dwFlags = VolatileLoad(&m_dwFlags);
459+
*initResult = m_dwFlags & enum_flag_Initialized;
460+
return (dwFlags & (enum_flag_IsStaticDataAllocated|enum_flag_Initialized)) != 0;
461+
}
462+
453463
#ifndef DACCESS_COMPILE
454464
inline void SetClassInited()
455465
{
@@ -465,10 +475,10 @@ struct MethodTableAuxiliaryData
465475
}
466476

467477
#ifndef DACCESS_COMPILE
468-
inline void SetIsStaticDataAllocated()
478+
inline void SetIsStaticDataAllocated(bool markAsInitedToo)
469479
{
470480
LIMITED_METHOD_CONTRACT;
471-
InterlockedOr((LONG*)&m_dwFlags, (LONG)enum_flag_IsStaticDataAllocated);
481+
InterlockedOr((LONG*)&m_dwFlags, markAsInitedToo ? (LONG)(enum_flag_IsStaticDataAllocated|enum_flag_Initialized) : (LONG)enum_flag_IsStaticDataAllocated);
472482
}
473483
#endif
474484

@@ -567,7 +577,7 @@ struct DynamicStaticsInfo
567577
bool GetIsInitedAndNonGCStaticsPointerIfInited(PTR_BYTE *ptrResult) { TADDR staticsVal = VolatileLoadWithoutBarrier(&m_pNonGCStatics); *ptrResult = dac_cast<PTR_BYTE>(staticsVal); return !(staticsVal & ISCLASSNOTINITED); }
568578

569579
// This function sets the pointer portion of a statics pointer. It returns false if the statics value was already set.
570-
bool InterlockedUpdateStaticsPointer(bool isGC, TADDR newVal)
580+
bool InterlockedUpdateStaticsPointer(bool isGC, TADDR newVal, bool isClassInitedByUpdatingStaticPointer)
571581
{
572582
TADDR oldVal;
573583
TADDR oldValFromInterlockedOp;
@@ -583,7 +593,14 @@ struct DynamicStaticsInfo
583593
return false;
584594
}
585595

586-
oldValFromInterlockedOp = InterlockedCompareExchangeT(pAddr, newVal | oldVal, oldVal);
596+
if (isClassInitedByUpdatingStaticPointer)
597+
{
598+
oldValFromInterlockedOp = InterlockedCompareExchangeT(pAddr, newVal, oldVal);
599+
}
600+
else
601+
{
602+
oldValFromInterlockedOp = InterlockedCompareExchangeT(pAddr, newVal | oldVal, oldVal);
603+
}
587604
} while(oldValFromInterlockedOp != oldVal);
588605
return true;
589606
}
@@ -1042,18 +1059,31 @@ class MethodTable
10421059
#ifndef DACCESS_COMPILE
10431060
void SetClassInited()
10441061
{
1045-
GetAuxiliaryDataForWrite()->SetClassInited();
1062+
// This must be before setting the MethodTable level flag, as otherwise there is a race condition where
1063+
// the MethodTable flag is set, which would allows the JIT to generate a call to a helper which assumes
1064+
// the DynamicStaticInfo level flag is set.
1065+
// The other race in the other direction is not a concern, as it can only cause allows reads/write from the static
1066+
// fields, which are effectively inited in any case once we reach this point.
10461067
if (IsDynamicStatics())
10471068
{
10481069
GetDynamicStaticsInfo()->SetClassInited();
10491070
}
1071+
GetAuxiliaryDataForWrite()->SetClassInited();
10501072
}
10511073

1052-
void AttemptToPreinit();
1074+
private:
1075+
bool IsInitedIfStaticDataAllocated();
1076+
public:
1077+
// Is the MethodTable current initialized, and/or can the runtime initialize the MethodTable
1078+
// without running any user code. (This function may allocate memory, and may throw OutOfMemory)
1079+
bool IsClassInitedOrPreinited();
10531080
#endif
10541081

1082+
// Is the MethodTable current known to be initialized
1083+
// If you want to know if it is initialized and allocation/throwing is permitted, call IsClassInitedOrPreinited instead
10551084
BOOL IsClassInited()
10561085
{
1086+
LIMITED_METHOD_DAC_CONTRACT;
10571087
return GetAuxiliaryDataForWrite()->IsClassInited();
10581088
}
10591089

src/coreclr/vm/prestub.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3545,9 +3545,8 @@ static PCODE getHelperForStaticBase(Module * pModule, CORCOMPILE_FIXUP_BLOB_KIND
35453545
{
35463546
STANDARD_VM_CONTRACT;
35473547

3548-
pMT->AttemptToPreinit();
35493548
bool GCStatic = (kind == ENCODE_STATIC_BASE_GC_HELPER || kind == ENCODE_THREAD_STATIC_BASE_GC_HELPER);
3550-
bool noCtor = pMT->IsClassInited();
3549+
bool noCtor = pMT->IsClassInitedOrPreinited();
35513550
bool threadStatic = (kind == ENCODE_THREAD_STATIC_BASE_NONGC_HELPER || kind == ENCODE_THREAD_STATIC_BASE_GC_HELPER);
35523551

35533552
CorInfoHelpFunc helper;
@@ -3916,7 +3915,7 @@ PCODE DynamicHelperFixup(TransitionBlock * pTransitionBlock, TADDR * pCell, DWOR
39163915
else
39173916
{
39183917
// Delay the creation of the helper until the type is initialized
3919-
if (pMT->IsClassInited())
3918+
if (pMT->IsClassInitedOrPreinited())
39203919
pHelper = getHelperForInitializedStatic(pModule, (CORCOMPILE_FIXUP_BLOB_KIND)kind, pMT, pFD);
39213920
}
39223921
}

0 commit comments

Comments
 (0)