Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoped cache broken due to operator== ABA problem #409

Merged
merged 5 commits into from
Oct 14, 2023
Merged

Conversation

bitfaster
Copy link
Owner

@bitfaster bitfaster commented Oct 14, 2023

See the wikipedia description for the ABA problem.

At some point, ReferenceCount was updated to implement IEquatable (possibly because code analysis said this is a good idea). This changed operator == from the default which tests whether two references of that type refer to the same object, to comparing for field equality.

This broke the Scoped class. Specifically, this line and this line are now susceptible to the ABA problem because the equality operator is checking field values:

if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount))

This issue does not affect SingletonCache, because it does not depend on operator== or operator!=.

There was a further bug in Scoped.DecrementReferenceCount() caused by reading a stale value of this.refCount.Count after it was replaced by a compare exchange call.

@bitfaster bitfaster changed the title Scoped cache broken due to ABA problem Scoped cache broken due to equals + ABA problem Oct 14, 2023
@bitfaster bitfaster changed the title Scoped cache broken due to equals + ABA problem Scoped cache broken due to operator== ABA problem Oct 14, 2023
@coveralls
Copy link

coveralls commented Oct 14, 2023

Coverage Status

coverage: 97.3%. first build when pulling 218443e on users/alexpeck/aba into 5ffc05a on main.

@@ -45,7 +45,7 @@ public bool TryCreateLifetime(out Lifetime<T> lifetime)
var oldRefCount = this.refCount;

// If old ref count is 0, the scoped object has been disposed and there was a race.
if (this.isDisposed || oldRefCount.Count == 0)
if (IsDisposed || oldRefCount.Count == 0)
Copy link
Owner Author

Choose a reason for hiding this comment

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

I think there is another potential race as follows:

  1. Thread A: check IsDisposed returns false
  2. Thread B: Dispose, IsDisposed now true
  3. Thread A: IncrementCount, return lifetime containing disposed value

I am not able to provoke this case with the soak test.

@bitfaster bitfaster merged commit c37109c into main Oct 14, 2023
@bitfaster bitfaster deleted the users/alexpeck/aba branch October 14, 2023 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants