Skip to content

Commit 677c557

Browse files
github-actions[bot]madelsoncarlossanlopbuyaa-n
authored
[release/6.0] Fix thread-safety issues with enumerating ResourceManager. (#81281)
* Fix thread-safety issues with enumerating ResourceManager. Concurrently enumerating a ResourceManager while also calling GetString() and similar methods was prone to both transient errors and deadlock. The transient errors were caused by RuntimeResourceSet calling internal methods on ResourceReader that did not properly lock. Now, all exposed methods on the Reader are thread-safe. The deadlock was called by inconsistent lock ordering between ResourceReader.ResourceEnumerator and RuntimeResourceSet which both lock on the RuntimeResourceSet's cache and on the ResourceReader itself. Now, the enumerator does not need to take both locks at the same time. Fix #74052 Fix #74868 * Remove trailing whitespace * Address feedback from #75054 * Add comment in response to #75054 (comment) * Implement feedback from #75054 * Increase timeout for TestResourceManagerIsSafeForConcurrentAccessAndEnumeration (#80330) This raises the timeout to 30s, the same as what we have for the equivalent ResourceManager test (https://github.com/dotnet/runtime/blob/15fcb990fe17348ab6ddde0939200b900169920b/src/libraries/System.Resources.ResourceManager/tests/ResourceManagerTests.cs#L255). fix #80277 --------- Co-authored-by: Michael Adelson <[email protected]> Co-authored-by: madelson <[email protected]> Co-authored-by: Carlos Sanchez <[email protected]> Co-authored-by: Buyaa Namnan <[email protected]>
1 parent 6dcc744 commit 677c557

File tree

7 files changed

+704
-90
lines changed

7 files changed

+704
-90
lines changed

src/libraries/System.Private.CoreLib/src/System/Resources/ResourceReader.cs

Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.IO;
1010
using System.Runtime.CompilerServices;
1111
using System.Text;
12+
using System.Threading;
1213

1314
namespace System.Resources
1415
#if RESOURCES_EXTENSIONS
@@ -58,10 +59,11 @@ public sealed partial class
5859
// it make sense to use anything less than one page?
5960
private const int DefaultFileStreamBufferSize = 4096;
6061

61-
private BinaryReader _store; // backing store we're reading from.
62-
// Used by RuntimeResourceSet and this class's enumerator. Maps
63-
// resource name to a value, a ResourceLocator, or a
64-
// LooselyLinkedManifestResource.
62+
// Backing store we're reading from. Usages outside of constructor
63+
// initialization must be protected by lock (this).
64+
private BinaryReader _store;
65+
// Used by RuntimeResourceSet and this class's enumerator.
66+
// Accesses must be protected by lock(_resCache).
6567
internal Dictionary<string, ResourceLocator>? _resCache;
6668
private long _nameSectionOffset; // Offset to name section of file.
6769
private long _dataSectionOffset; // Offset to Data section of file.
@@ -86,7 +88,6 @@ public sealed partial class
8688
// Version number of .resources file, for compatibility
8789
private int _version;
8890

89-
9091
public
9192
#if RESOURCES_EXTENSIONS
9293
DeserializingResourceReader(string fileName)
@@ -163,13 +164,16 @@ private unsafe void Dispose(bool disposing)
163164
}
164165
}
165166

166-
internal static unsafe int ReadUnalignedI4(int* p)
167+
private static unsafe int ReadUnalignedI4(int* p)
167168
{
168169
return BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(p, sizeof(int)));
169170
}
170171

171172
private void SkipString()
172173
{
174+
// Note: this method assumes that it is called either during object
175+
// construction or within another method that locks on this.
176+
173177
int stringLength = _store.Read7BitEncodedInt();
174178
if (stringLength < 0)
175179
{
@@ -228,6 +232,7 @@ public IDictionaryEnumerator GetEnumerator()
228232
return new ResourceEnumerator(this);
229233
}
230234

235+
// Called from RuntimeResourceSet
231236
internal ResourceEnumerator GetEnumeratorInternal()
232237
{
233238
return new ResourceEnumerator(this);
@@ -237,6 +242,7 @@ internal ResourceEnumerator GetEnumeratorInternal()
237242
// To read the data, seek to _dataSectionOffset + dataPos, then
238243
// read the resource type & data.
239244
// This does a binary search through the names.
245+
// Called from RuntimeResourceSet
240246
internal int FindPosForResource(string name)
241247
{
242248
Debug.Assert(_store != null, "ResourceReader is closed!");
@@ -321,6 +327,8 @@ internal int FindPosForResource(string name)
321327
private unsafe bool CompareStringEqualsName(string name)
322328
{
323329
Debug.Assert(_store != null, "ResourceReader is closed!");
330+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
331+
324332
int byteLen = _store.Read7BitEncodedInt();
325333
if (byteLen < 0)
326334
{
@@ -453,68 +461,74 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
453461
}
454462

455463
// This takes a virtual offset into the data section and reads a String
456-
// from that location.
457-
// Anyone who calls LoadObject should make sure they take a lock so
458-
// no one can cause us to do a seek in here.
464+
// from that location. Called from RuntimeResourceSet
459465
internal string? LoadString(int pos)
460466
{
461467
Debug.Assert(_store != null, "ResourceReader is closed!");
462-
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
463-
string? s = null;
464-
int typeIndex = _store.Read7BitEncodedInt();
465-
if (_version == 1)
466-
{
467-
if (typeIndex == -1)
468-
return null;
469-
if (FindType(typeIndex) != typeof(string))
470-
throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, FindType(typeIndex).FullName));
471-
s = _store.ReadString();
472-
}
473-
else
468+
469+
lock (this)
474470
{
475-
ResourceTypeCode typeCode = (ResourceTypeCode)typeIndex;
476-
if (typeCode != ResourceTypeCode.String && typeCode != ResourceTypeCode.Null)
471+
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
472+
string? s = null;
473+
int typeIndex = _store.Read7BitEncodedInt();
474+
if (_version == 1)
477475
{
478-
string? typeString;
479-
if (typeCode < ResourceTypeCode.StartOfUserTypes)
480-
typeString = typeCode.ToString();
481-
else
482-
typeString = FindType(typeCode - ResourceTypeCode.StartOfUserTypes).FullName;
483-
throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, typeString));
484-
}
485-
if (typeCode == ResourceTypeCode.String) // ignore Null
476+
if (typeIndex == -1)
477+
return null;
478+
if (FindType(typeIndex) != typeof(string))
479+
throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, FindType(typeIndex).FullName));
486480
s = _store.ReadString();
481+
}
482+
else
483+
{
484+
ResourceTypeCode typeCode = (ResourceTypeCode)typeIndex;
485+
if (typeCode != ResourceTypeCode.String && typeCode != ResourceTypeCode.Null)
486+
{
487+
string? typeString;
488+
if (typeCode < ResourceTypeCode.StartOfUserTypes)
489+
typeString = typeCode.ToString();
490+
else
491+
typeString = FindType(typeCode - ResourceTypeCode.StartOfUserTypes).FullName;
492+
throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ResourceNotString_Type, typeString));
493+
}
494+
if (typeCode == ResourceTypeCode.String) // ignore Null
495+
s = _store.ReadString();
496+
}
497+
return s;
487498
}
488-
return s;
489499
}
490500

491501
// Called from RuntimeResourceSet
492502
internal object? LoadObject(int pos)
493503
{
494-
if (_version == 1)
495-
return LoadObjectV1(pos);
496-
return LoadObjectV2(pos, out _);
504+
lock (this)
505+
{
506+
return _version == 1 ? LoadObjectV1(pos) : LoadObjectV2(pos, out _);
507+
}
497508
}
498509

510+
// Called from RuntimeResourceSet
499511
internal object? LoadObject(int pos, out ResourceTypeCode typeCode)
500512
{
501-
if (_version == 1)
513+
lock (this)
502514
{
503-
object? o = LoadObjectV1(pos);
504-
typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes;
505-
return o;
515+
if (_version == 1)
516+
{
517+
object? o = LoadObjectV1(pos);
518+
typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes;
519+
return o;
520+
}
521+
return LoadObjectV2(pos, out typeCode);
506522
}
507-
return LoadObjectV2(pos, out typeCode);
508523
}
509524

510525
// This takes a virtual offset into the data section and reads an Object
511526
// from that location.
512-
// Anyone who calls LoadObject should make sure they take a lock so
513-
// no one can cause us to do a seek in here.
514-
internal object? LoadObjectV1(int pos)
527+
private object? LoadObjectV1(int pos)
515528
{
516529
Debug.Assert(_store != null, "ResourceReader is closed!");
517530
Debug.Assert(_version == 1, ".resources file was not a V1 .resources file!");
531+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
518532

519533
try
520534
{
@@ -534,6 +548,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
534548

535549
private object? _LoadObjectV1(int pos)
536550
{
551+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
552+
537553
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
538554
int typeIndex = _store.Read7BitEncodedInt();
539555
if (typeIndex == -1)
@@ -586,10 +602,11 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
586602
}
587603
}
588604

589-
internal object? LoadObjectV2(int pos, out ResourceTypeCode typeCode)
605+
private object? LoadObjectV2(int pos, out ResourceTypeCode typeCode)
590606
{
591607
Debug.Assert(_store != null, "ResourceReader is closed!");
592608
Debug.Assert(_version >= 2, ".resources file was not a V2 (or higher) .resources file!");
609+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
593610

594611
try
595612
{
@@ -609,6 +626,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
609626

610627
private object? _LoadObjectV2(int pos, out ResourceTypeCode typeCode)
611628
{
629+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
630+
612631
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
613632
typeCode = (ResourceTypeCode)_store.Read7BitEncodedInt();
614633

@@ -745,6 +764,7 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
745764
[MemberNotNull(nameof(_typeNamePositions))]
746765
private void ReadResources()
747766
{
767+
Debug.Assert(!Monitor.IsEntered(this)); // only called during init
748768
Debug.Assert(_store != null, "ResourceReader is closed!");
749769

750770
try
@@ -767,6 +787,8 @@ private void ReadResources()
767787
[MemberNotNull(nameof(_typeNamePositions))]
768788
private void _ReadResources()
769789
{
790+
Debug.Assert(!Monitor.IsEntered(this)); // only called during init
791+
770792
// Read ResourceManager header
771793
// Check for magic number
772794
int magicNum = _store.ReadInt32();
@@ -952,6 +974,8 @@ private Type FindType(int typeIndex)
952974
"Custom readers as well as custom objects on the resources file are not observable by the trimmer and so required assemblies, types and members may be removed.")]
953975
private Type UseReflectionToGetType(int typeIndex)
954976
{
977+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
978+
955979
long oldPos = _store.BaseStream.Position;
956980
try
957981
{
@@ -984,6 +1008,8 @@ private Type UseReflectionToGetType(int typeIndex)
9841008
private string TypeNameFromTypeCode(ResourceTypeCode typeCode)
9851009
{
9861010
Debug.Assert(typeCode >= 0, "can't be negative");
1011+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
1012+
9871013
if (typeCode < ResourceTypeCode.StartOfUserTypes)
9881014
{
9891015
Debug.Assert(!string.Equals(typeCode.ToString(), "LastPrimitive"), "Change ResourceTypeCode metadata order so LastPrimitive isn't what Enum.ToString prefers.");
@@ -1061,31 +1087,31 @@ public DictionaryEntry Entry
10611087
if (!_currentIsValid) throw new InvalidOperationException(SR.InvalidOperation_EnumNotStarted);
10621088
if (_reader._resCache == null) throw new InvalidOperationException(SR.ResourceReaderIsClosed);
10631089

1064-
string key;
1090+
string key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader
1091+
10651092
object? value = null;
1066-
lock (_reader)
1067-
{ // locks should be taken in the same order as in RuntimeResourceSet.GetObject to avoid deadlock
1068-
lock (_reader._resCache)
1093+
// Lock the cache first, then the reader (in this case, we don't actually need to lock the reader and cache at the same time).
1094+
// Lock order MUST match RuntimeResourceSet.GetObject to avoid deadlock.
1095+
Debug.Assert(!Monitor.IsEntered(_reader));
1096+
lock (_reader._resCache)
1097+
{
1098+
if (_reader._resCache.TryGetValue(key, out ResourceLocator locator))
10691099
{
1070-
key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader
1071-
if (_reader._resCache.TryGetValue(key, out ResourceLocator locator))
1072-
{
1073-
value = locator.Value;
1074-
}
1075-
if (value == null)
1076-
{
1077-
if (_dataPosition == -1)
1078-
value = _reader.GetValueForNameIndex(_currentName);
1079-
else
1080-
value = _reader.LoadObject(_dataPosition);
1081-
// If enumeration and subsequent lookups happen very
1082-
// frequently in the same process, add a ResourceLocator
1083-
// to _resCache here. But WinForms enumerates and
1084-
// just about everyone else does lookups. So caching
1085-
// here may bloat working set.
1086-
}
1100+
value = locator.Value;
10871101
}
10881102
}
1103+
if (value is null)
1104+
{
1105+
if (_dataPosition == -1)
1106+
value = _reader.GetValueForNameIndex(_currentName);
1107+
else
1108+
value = _reader.LoadObject(_dataPosition);
1109+
// If enumeration and subsequent lookups happen very
1110+
// frequently in the same process, add a ResourceLocator
1111+
// to _resCache here (we'll also need to extend the lock block!).
1112+
// But WinForms enumerates and just about everyone else does lookups.
1113+
// So caching here may bloat working set.
1114+
}
10891115
return new DictionaryEntry(key, value);
10901116
}
10911117
}

src/libraries/System.Private.CoreLib/src/System/Resources/RuntimeResourceSet.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.IO;
8+
using System.Threading;
89

910
namespace System.Resources
1011
#if RESOURCES_EXTENSIONS
@@ -276,6 +277,9 @@ private IDictionaryEnumerator GetEnumeratorHelper()
276277
object? value;
277278
ResourceLocator resEntry;
278279

280+
// Lock the cache first, then the reader (reader locks implicitly through its methods).
281+
// Lock order MUST match ResourceReader.ResourceEnumerator.Entry to avoid deadlock.
282+
Debug.Assert(!Monitor.IsEntered(reader));
279283
lock (cache)
280284
{
281285
// Find the offset within the data section
@@ -288,7 +292,7 @@ private IDictionaryEnumerator GetEnumeratorHelper()
288292

289293
// When data type cannot be cached
290294
dataPos = resEntry.DataPosition;
291-
return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos, out _);
295+
return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos);
292296
}
293297

294298
dataPos = reader.FindPosForResource(key);
@@ -346,14 +350,11 @@ private IDictionaryEnumerator GetEnumeratorHelper()
346350
return value;
347351
}
348352

349-
private static object? ReadValue (ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator)
353+
private static object? ReadValue(ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator)
350354
{
351355
object? value;
352356
ResourceTypeCode typeCode;
353357

354-
// Normally calling LoadString or LoadObject requires
355-
// taking a lock. Note that in this case, we took a
356-
// lock before calling this method.
357358
if (isString)
358359
{
359360
value = reader.LoadString(dataPos);

0 commit comments

Comments
 (0)