Skip to content

Conversation

@madelson
Copy link
Contributor

@madelson madelson commented Sep 3, 2022

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

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 dotnet#74052
Fix dotnet#74868
@ghost ghost added area-System.Resources community-contribution Indicates that the PR has been added by a community member labels Sep 3, 2022
@ghost
Copy link

ghost commented Sep 3, 2022

Tagging subscribers to this area: @dotnet/area-system-resources
See info in area-owners.md if you want to be subscribed.

Issue Details

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

Author: madelson
Assignees: -
Labels:

area-System.Resources

Milestone: -

"the user to only get one error.")]
private object DeserializeObject(int typeIndex)
{
Debug.Assert(Monitor.IsEntered(this));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these to private methods that access _store per this suggestion.


// Normally calling LoadString or LoadObject requires
// taking a lock. Note that in this case, we took a
// lock before calling this method.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was not (no longer?) correct. We do typically call these within a lock, but it is a lock on either _resCache or _caseInsensitiveTable. That lock was previously making things thread-safe so long as the caller was only using GetString/GetObject and was not flipping ResourceManager.IgnoreCase on and off.

Now, LoadString/LoadObject do lock internally so there is no edge case here.

using Barrier barrier = new(Threads);
Task task = Task.WhenAll(Enumerable.Range(0, Threads).Select(_ => Task.Run(WaitForBarrierThenEnumerateResources)));

Assert.True(task.Wait(TimeSpan.FromSeconds(10)));
Copy link
Contributor Author

@madelson madelson Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before my fixes, this test reliably would reproduce both issues on my machine. Let me know if there is a better way to test for concurrency issues like this.

I do think it is very important to have such a test, because this problem was actually previously fixed in 2015 and then broken again in 2020 (I think the break happened here).

<Content Include="Resources\icon.ico"
Link="%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reformatting was done by VS

<EmbeddedResource Include="Resources\AToZResx.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AToZResx.Designer.cs</LastGenOutput>
</EmbeddedResource>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what VS generated when I made a new resx file. Any reason to change it to match what the other files were using?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm reading this again I think it is consistent with the others

@madelson madelson requested a review from stephentoub September 6, 2022 23:40
@joperezr joperezr requested a review from ericstj September 29, 2022 22:58
@ericstj ericstj removed their request for review November 7, 2022 20:11
"the user to only get one error.")]
private object DeserializeObject(int typeIndex)
{
Debug.Assert(Monitor.IsEntered(this));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add a comment explaining why this is being added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After double checking I don't think we need this one; this method doesn't access _store directly. Removed

"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.")]
private bool InitializeBinaryFormatter()
{
Debug.Assert(Monitor.IsEntered(this));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. That way in the future people that look at this code will know why is the protection expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this one too. Not needed.

[Theory]
[InlineData(false)]
[InlineData(true)]
public static void TestResourceManagerIsSafeForConcurrentAccessAndEnumeration(bool useEnumeratorEntry)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to add equivalent tests in System.Resources.Extensions as well and perhaps with a resource file that has something other than strings?

Copy link
Member

@joperezr joperezr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay in reviewing this @madelson, I've left some comments but overall this is looking good, thanks for the contribution.

@madelson
Copy link
Contributor Author

madelson commented Nov 9, 2022

I've left some comments but overall this is looking good

Thanks @joperezr . I've pushed a change addressing your feedback. Let me know if there is anything else to do.

@joperezr
Copy link
Member

joperezr commented Nov 15, 2022

    private void SkipString()

What about private void SkipString()? That one seems to be using _store as well.

@joperezr
Copy link
Member

Same for private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) that one also seems to be accessing _store

@joperezr
Copy link
Member

So following the lines of my last two comments, there seems to be two different approaches when _store gets accessed outside of the constructor:

  • You use lock(this)
  • You don't need to use lock(this) given the method could only be called by something that is already locked, so you add a comment to mention that on the method.

There are some places where we still use _store and are not doing either of the above (my assumption is the fall into the second category), so I t hink it would be good to add a comment to those places to explain why is locking not required, as the comment you added on _store field itself requires this.

@madelson
Copy link
Contributor Author

What about private void SkipString()? That one seems to be using _store as well.

@joperezr this one can't have an assertion because it is called both during initialization (which per my comment on _store does not use locking) and post-initialization.

Do you think it is worth adding a comment like this to SkipString()?:

// Note: this method assumes that it is called either from the constructor or within another method that locks on this

Same for private unsafe string AllocateStringForNameIndex(int index, out int dataOffset) that one also seems to be accessing _store

This one accesses _store but note that the access is wrapped in a lock(this) block within that method. I only added assertions to methods that make assumptions about locking happening in the caller.

There are some places where we still use _store and are not doing either of the above

I believe all of those places are called during initialization, which is called out by the comment on _store:

        // Backing store we're reading from. Usages outside of constructor
        // initialization must be protected by lock (this).

In cases which are always called during initialization (e.g. _ReadResources()) I assert that the lock is not taken.

@madelson madelson requested review from joperezr and removed request for stephentoub November 15, 2022 23:49
@joperezr
Copy link
Member

I see thanks for the explanation and sorry that I missed some of those comments.

Do you think it is worth adding a comment like this to SkipString()?:

Yes, I do think this would be good for consistency.

Copy link
Member

@joperezr joperezr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from my last NIT of just adding a comment to SkipString this change looks good to me. @stephentoub since you commented on this PR earlier, any thoughts on the reply from @madelson?


const int Threads = 10;
using Barrier barrier = new(Threads);
Task task = Task.WhenAll(Enumerable.Range(0, Threads).Select(_ => Task.Run(WaitForBarrierThenEnumerateResources)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the same comments as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doen

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments, but otherwise LGTM. Thanks.

@madelson
Copy link
Contributor Author

@jkotas
Copy link
Member

jkotas commented Dec 12, 2022

The new failure is #79517

@madelson
Copy link
Contributor Author

@joperezr anything else to do here?

@buyaa-n
Copy link
Contributor

buyaa-n commented Jan 27, 2023

/backport to release/6.0

@github-actions
Copy link
Contributor

Started backporting to release/6.0: https://github.com/dotnet/runtime/actions/runs/4027066289

github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
@joperezr
Copy link
Member

@buyaa-n assuming you also plan to backport to 7.0?

@buyaa-n
Copy link
Contributor

buyaa-n commented Jan 27, 2023

/backport to release/7.0

@github-actions
Copy link
Contributor

Started backporting to release/7.0: https://github.com/dotnet/runtime/actions/runs/4027191479

github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
github-actions bot pushed a commit that referenced this pull request Jan 27, 2023
carlossanlop pushed a commit that referenced this pull request Feb 10, 2023
carlossanlop pushed a commit that referenced this pull request Feb 10, 2023
carlossanlop pushed a commit that referenced this pull request Feb 10, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Feb 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-System.Resources community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deadlock when enumerating resources with ResourceManager System.Resources.ResourceManager is not thread safe when ignore case is enabled

5 participants