-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
RuntimeHelpers.Equals incorrectly compares an extra four bytes on x64 #98875
Comments
Great find! Object's header (8 bytes) should never contain any garbage in its upper 4 bytes, but, presumably, garbage values could be in the padding between objects (due to 8-byte alignment) since GC supports allocations on unitialized memory (e.g. |
#97174 could be related |
@EgorBo I really want this API to stay. It's essential for comparing boxed structs that contain references without deferring to how the struct defines Equals. (I've had bugs due to Equals being a looser comparison, e.g. with System.Decimal.)
One example of where I use it is in INotifyPropertyChanged comparisons, for which it's the perfect answer to loose Equals overrides causing bugs, again due to the nature of how it answers "could a difference be seen if this assignment were to be done." It's also useful in other scenarios. The fix looks as straightforward as they come, so I'm very hopeful that it gets fixed in .NET Framework and .NET 6+. |
Minimal repro (net8.0, Release, Windows-11 x64): using System.Runtime.CompilerServices;
long i = 0;
while (true)
{
i++;
Guid g1 = default;
Guid g2 = default;
if (!RuntimeHelpers.Equals(g1, g2))
throw new Exception($" failed, i={i}");
} |
Tagging subscribers to this area: @dotnet/area-system-runtime-compilerservices Issue DetailsDescriptionDiscovered in .NET Framework 4.8 and still present in .NET 8.0. (Please fix in .NET Framework as well.)
runtime/src/coreclr/classlibnative/bcltype/objectnative.cpp Lines 160 to 167 in 00874d6
That Reproduction StepsFor .NET Framework, where the bug was found, I'm not how to set up a repro. Four bytes past the end of most boxed values is often going to go into the object header of another object, and the object header of another object on x64 is four zero bytes for padding followed by four sync block bytes. Those four padding bytes are probably why repros are so hard to come by. There must have been a larger object that was freed, and this boxed object was allocated over memory that had not been zeroed. My team members and I have been seeing this bug in RuntimeHelpers.Equals every month or two where it incorrectly returns false and triggers one of our debug assertions. When it incorrectly returns false, I verified that the boxed values being compared are identical in raw memory, but the four bytes following the boxed object do differ from zero in the cases where RuntimeHelpers.Equals is incorrectly returning false. For .NET 6 and 8, this repro works instantly (thanks @EgorBo): using System.Runtime.CompilerServices;
while (true)
{
if (!RuntimeHelpers.Equals(default(Guid), default(Guid)))
throw new Exception("!");
} Expected behaviorRuntimeHelpers.Equals should compare only the instance data of the boxed values. Actual behaviorRuntimeHelpers.Equals appears to compare an additional four bytes past the end of the instance data of the boxed values. Regression?This is broken in .NET Framework 4.8 as well. Known WorkaroundsNo workarounds known since comparing memory inside boxed structs is not safe without pausing GCs. ConfigurationNo response Other informationNo response
|
Closed via #97641 in Main |
Requested fix in .NET Framework here: https://developercommunity.visualstudio.com/t/RuntimeHelpersEquals-compares-invalid-m/10602858 |
The bar for servicing fixes is high (especially for .NET Framework). We don't expect this API to be popular (in fact, I only found a few uses) + we consider it as a quite dangerous API since it may produce unexpected/random results when structs have paddings/gaps or floating point fields. So IMO it does not meet that bar, unless other folks have a different opinion |
Right now, it's the only API that "sees through" boxed copies of struct values without calling customized Equals logic. Boxing happens as part of data binding, e.g. PropertyDescriptor.GetValue, and during my usage of RuntimeHelpers.Equals, there have been no issues with struct field gaps thus far. I understand the concerns, but it's also providing a valuable feature that has no alternative. For instance, there's a need to distinguish whether an assignment will change In general, the needed API says: "could an assignment of Is it possible to reproduce an issue where two different boxed copies of the same struct value have different contents in gaps? |
Description
Discovered in .NET Framework 4.8 and still present in .NET 8.0. (Please fix in .NET Framework as well.)
sizeof(int)
(4) is subtracted instead 8 for x64, causing an extra four bytes past the end of the boxed struct value to be compared:runtime/src/coreclr/classlibnative/bcltype/objectnative.cpp
Lines 160 to 167 in 00874d6
That
sizeof(int)
line was migrated as-is when this Git history started.Reproduction Steps
For .NET Framework, where the bug was found, I'm not how to set up a repro. Four bytes past the end of most boxed values is often going to go into the object header of another object, and the object header of another object on x64 is four zero bytes for padding followed by four sync block bytes. Those four padding bytes are probably why repros are so hard to come by. There must have been a larger object that was freed, and this boxed object was allocated over memory that had not been zeroed.
My team members and I have been seeing this bug in RuntimeHelpers.Equals every month or two where it incorrectly returns false and triggers one of our debug assertions. When it incorrectly returns false, I verified that the boxed values being compared are identical in raw memory, but the four bytes following the boxed object do differ from zero in the cases where RuntimeHelpers.Equals is incorrectly returning false.
For .NET 6 and 8, this repro works instantly (thanks @EgorBo):
Expected behavior
RuntimeHelpers.Equals should compare only the instance data of the boxed values.
Actual behavior
RuntimeHelpers.Equals appears to compare an additional four bytes past the end of the instance data of the boxed values.
Regression?
This is broken in .NET Framework 4.8 as well.
Known Workarounds
No workarounds known since comparing memory inside boxed structs is not safe without pausing GCs.
Configuration
No response
Other information
No response
The text was updated successfully, but these errors were encountered: