Skip to content

Commit 6e6eab2

Browse files
github-actions[bot]madelsonbuyaa-n
authored
[release/7.0] Fix thread-safety issues with enumerating ResourceManager. (#81283)
* 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: Buyaa Namnan <[email protected]>
1 parent 6eaaf26 commit 6e6eab2

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
@@ -60,10 +61,11 @@ public sealed partial class
6061
// it make sense to use anything less than one page?
6162
private const int DefaultFileStreamBufferSize = 4096;
6263

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

91-
9293
public
9394
#if RESOURCES_EXTENSIONS
9495
DeserializingResourceReader(string fileName)
@@ -169,13 +170,16 @@ private unsafe void Dispose(bool disposing)
169170
}
170171
}
171172

172-
internal static unsafe int ReadUnalignedI4(int* p)
173+
private static unsafe int ReadUnalignedI4(int* p)
173174
{
174175
return BinaryPrimitives.ReadInt32LittleEndian(new ReadOnlySpan<byte>(p, sizeof(int)));
175176
}
176177

177178
private void SkipString()
178179
{
180+
// Note: this method assumes that it is called either during object
181+
// construction or within another method that locks on this.
182+
179183
int stringLength = _store.Read7BitEncodedInt();
180184
if (stringLength < 0)
181185
{
@@ -234,6 +238,7 @@ public IDictionaryEnumerator GetEnumerator()
234238
return new ResourceEnumerator(this);
235239
}
236240

241+
// Called from RuntimeResourceSet
237242
internal ResourceEnumerator GetEnumeratorInternal()
238243
{
239244
return new ResourceEnumerator(this);
@@ -243,6 +248,7 @@ internal ResourceEnumerator GetEnumeratorInternal()
243248
// To read the data, seek to _dataSectionOffset + dataPos, then
244249
// read the resource type & data.
245250
// This does a binary search through the names.
251+
// Called from RuntimeResourceSet
246252
internal int FindPosForResource(string name)
247253
{
248254
Debug.Assert(_store != null, "ResourceReader is closed!");
@@ -327,6 +333,8 @@ internal int FindPosForResource(string name)
327333
private unsafe bool CompareStringEqualsName(string name)
328334
{
329335
Debug.Assert(_store != null, "ResourceReader is closed!");
336+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
337+
330338
int byteLen = _store.Read7BitEncodedInt();
331339
if (byteLen < 0)
332340
{
@@ -459,68 +467,74 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
459467
}
460468

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

497507
// Called from RuntimeResourceSet
498508
internal object? LoadObject(int pos)
499509
{
500-
if (_version == 1)
501-
return LoadObjectV1(pos);
502-
return LoadObjectV2(pos, out _);
510+
lock (this)
511+
{
512+
return _version == 1 ? LoadObjectV1(pos) : LoadObjectV2(pos, out _);
513+
}
503514
}
504515

516+
// Called from RuntimeResourceSet
505517
internal object? LoadObject(int pos, out ResourceTypeCode typeCode)
506518
{
507-
if (_version == 1)
519+
lock (this)
508520
{
509-
object? o = LoadObjectV1(pos);
510-
typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes;
511-
return o;
521+
if (_version == 1)
522+
{
523+
object? o = LoadObjectV1(pos);
524+
typeCode = (o is string) ? ResourceTypeCode.String : ResourceTypeCode.StartOfUserTypes;
525+
return o;
526+
}
527+
return LoadObjectV2(pos, out typeCode);
512528
}
513-
return LoadObjectV2(pos, out typeCode);
514529
}
515530

516531
// This takes a virtual offset into the data section and reads an Object
517532
// from that location.
518-
// Anyone who calls LoadObject should make sure they take a lock so
519-
// no one can cause us to do a seek in here.
520-
internal object? LoadObjectV1(int pos)
533+
private object? LoadObjectV1(int pos)
521534
{
522535
Debug.Assert(_store != null, "ResourceReader is closed!");
523536
Debug.Assert(_version == 1, ".resources file was not a V1 .resources file!");
537+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
524538

525539
try
526540
{
@@ -540,6 +554,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
540554

541555
private object? _LoadObjectV1(int pos)
542556
{
557+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
558+
543559
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
544560
int typeIndex = _store.Read7BitEncodedInt();
545561
if (typeIndex == -1)
@@ -596,10 +612,11 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
596612
}
597613
}
598614

599-
internal object? LoadObjectV2(int pos, out ResourceTypeCode typeCode)
615+
private object? LoadObjectV2(int pos, out ResourceTypeCode typeCode)
600616
{
601617
Debug.Assert(_store != null, "ResourceReader is closed!");
602618
Debug.Assert(_version >= 2, ".resources file was not a V2 (or higher) .resources file!");
619+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
603620

604621
try
605622
{
@@ -619,6 +636,8 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
619636

620637
private object? _LoadObjectV2(int pos, out ResourceTypeCode typeCode)
621638
{
639+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
640+
622641
_store.BaseStream.Seek(_dataSectionOffset + pos, SeekOrigin.Begin);
623642
typeCode = (ResourceTypeCode)_store.Read7BitEncodedInt();
624643

@@ -755,6 +774,7 @@ private unsafe string AllocateStringForNameIndex(int index, out int dataOffset)
755774
[MemberNotNull(nameof(_typeNamePositions))]
756775
private void ReadResources()
757776
{
777+
Debug.Assert(!Monitor.IsEntered(this)); // only called during init
758778
Debug.Assert(_store != null, "ResourceReader is closed!");
759779

760780
try
@@ -777,6 +797,8 @@ private void ReadResources()
777797
[MemberNotNull(nameof(_typeNamePositions))]
778798
private void _ReadResources()
779799
{
800+
Debug.Assert(!Monitor.IsEntered(this)); // only called during init
801+
780802
// Read ResourceManager header
781803
// Check for magic number
782804
int magicNum = _store.ReadInt32();
@@ -962,6 +984,8 @@ private Type FindType(int typeIndex)
962984
"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.")]
963985
private Type UseReflectionToGetType(int typeIndex)
964986
{
987+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
988+
965989
long oldPos = _store.BaseStream.Position;
966990
try
967991
{
@@ -1000,6 +1024,8 @@ private Type UseReflectionToGetType(int typeIndex)
10001024
private string TypeNameFromTypeCode(ResourceTypeCode typeCode)
10011025
{
10021026
Debug.Assert(typeCode >= 0, "can't be negative");
1027+
Debug.Assert(Monitor.IsEntered(this)); // uses _store
1028+
10031029
if (typeCode < ResourceTypeCode.StartOfUserTypes)
10041030
{
10051031
Debug.Assert(!string.Equals(typeCode.ToString(), "LastPrimitive"), "Change ResourceTypeCode metadata order so LastPrimitive isn't what Enum.ToString prefers.");
@@ -1077,31 +1103,31 @@ public DictionaryEntry Entry
10771103
if (!_currentIsValid) throw new InvalidOperationException(SR.InvalidOperation_EnumNotStarted);
10781104
if (_reader._resCache == null) throw new InvalidOperationException(SR.ResourceReaderIsClosed);
10791105

1080-
string key;
1106+
string key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader
1107+
10811108
object? value = null;
1082-
lock (_reader)
1083-
{ // locks should be taken in the same order as in RuntimeResourceSet.GetObject to avoid deadlock
1084-
lock (_reader._resCache)
1109+
// 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).
1110+
// Lock order MUST match RuntimeResourceSet.GetObject to avoid deadlock.
1111+
Debug.Assert(!Monitor.IsEntered(_reader));
1112+
lock (_reader._resCache)
1113+
{
1114+
if (_reader._resCache.TryGetValue(key, out ResourceLocator locator))
10851115
{
1086-
key = _reader.AllocateStringForNameIndex(_currentName, out _dataPosition); // AllocateStringForNameIndex could lock on _reader
1087-
if (_reader._resCache.TryGetValue(key, out ResourceLocator locator))
1088-
{
1089-
value = locator.Value;
1090-
}
1091-
if (value == null)
1092-
{
1093-
if (_dataPosition == -1)
1094-
value = _reader.GetValueForNameIndex(_currentName);
1095-
else
1096-
value = _reader.LoadObject(_dataPosition);
1097-
// If enumeration and subsequent lookups happen very
1098-
// frequently in the same process, add a ResourceLocator
1099-
// to _resCache here. But WinForms enumerates and
1100-
// just about everyone else does lookups. So caching
1101-
// here may bloat working set.
1102-
}
1116+
value = locator.Value;
11031117
}
11041118
}
1119+
if (value is null)
1120+
{
1121+
if (_dataPosition == -1)
1122+
value = _reader.GetValueForNameIndex(_currentName);
1123+
else
1124+
value = _reader.LoadObject(_dataPosition);
1125+
// If enumeration and subsequent lookups happen very
1126+
// frequently in the same process, add a ResourceLocator
1127+
// to _resCache here (we'll also need to extend the lock block!).
1128+
// But WinForms enumerates and just about everyone else does lookups.
1129+
// So caching here may bloat working set.
1130+
}
11051131
return new DictionaryEntry(key, value);
11061132
}
11071133
}

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
@@ -283,6 +284,9 @@ private IDictionaryEnumerator GetEnumeratorHelper()
283284
object? value;
284285
ResourceLocator resEntry;
285286

287+
// Lock the cache first, then the reader (reader locks implicitly through its methods).
288+
// Lock order MUST match ResourceReader.ResourceEnumerator.Entry to avoid deadlock.
289+
Debug.Assert(!Monitor.IsEntered(reader));
286290
lock (cache)
287291
{
288292
// Find the offset within the data section
@@ -295,7 +299,7 @@ private IDictionaryEnumerator GetEnumeratorHelper()
295299

296300
// When data type cannot be cached
297301
dataPos = resEntry.DataPosition;
298-
return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos, out _);
302+
return isString ? reader.LoadString(dataPos) : reader.LoadObject(dataPos);
299303
}
300304

301305
dataPos = reader.FindPosForResource(key);
@@ -353,14 +357,11 @@ private IDictionaryEnumerator GetEnumeratorHelper()
353357
return value;
354358
}
355359

356-
private static object? ReadValue (ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator)
360+
private static object? ReadValue(ResourceReader reader, int dataPos, bool isString, out ResourceLocator locator)
357361
{
358362
object? value;
359363
ResourceTypeCode typeCode;
360364

361-
// Normally calling LoadString or LoadObject requires
362-
// taking a lock. Note that in this case, we took a
363-
// lock before calling this method.
364365
if (isString)
365366
{
366367
value = reader.LoadString(dataPos);

0 commit comments

Comments
 (0)