Skip to content

RuntimeHelpers.Equals incorrectly compares an extra four bytes on x64 #98875

Closed
@jnm2

Description

@jnm2

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:

// Compare the contents (size - vtable - sync block index).
DWORD dwBaseSize = pThisMT->GetBaseSize();
if(pThisMT == g_pStringClass)
dwBaseSize -= sizeof(WCHAR);
BOOL ret = memcmp(
(void *) (pThisRef+1),
(void *) (pCompareRef+1),
dwBaseSize - sizeof(Object) - sizeof(int)) == 0;

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):

using System.Runtime.CompilerServices;

while (true)
{
    if (!RuntimeHelpers.Equals(default(Guid), default(Guid)))
        throw new Exception("!");
}

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions