Repository: dotnet/runtime
File: src/coreclr/vm/interpexec.cpp (C++)

Code before:

        // CAS has release semantics, so the fields of the entry have correct
        // values once the entry is published. If CAS succeeds, we own the old
        // entry for freeing
        InterpDispatchCacheEntry* pOldEntry = InterlockedExchangeT(&m_cache[idx], pNewEntry);

        if (pOldEntry != nullptr)
            AddToDeadList(pOldEntry);
    }

    // Called during GC sync point to free dead entries
    // At this point, no other threads are running, so it is safe to free
    void ReclaimDeadEntries()
    {
        LIMITED_METHOD_CONTRACT;

        InterpDispatchCacheEntry* pDeadList = m_pDeadList;

        while (pDeadList != nullptr)
        {
            InterpDispatchCacheEntry* pNext = pDeadList->pNextDead;
            delete pDeadList;
            pDeadList = pNext;
        }

        m_pDeadList = nullptr;
    }

    // Add an entry to the dead list for later cleanup
    void AddToDeadList(InterpDispatchCacheEntry* pEntry)
    {
        LIMITED_METHOD_CONTRACT;

        InterpDispatchCacheEntry* pOldHead;
        do
        {
            pOldHead = m_pDeadList;
            pEntry->pNextDead = pOldHead;
        }
        while (InterlockedCompareExchangeT(&m_pDeadList, pEntry, pOldHead) != pOldHead);
    }

    // Same as VSD DispatchCache's HashToken + HashMT
    static uint16_t Hash(size_t dispatchToken, void* pMT)
    {
        LIMITED_METHOD_CONTRACT;

        uint16_t dispatchTokenHash = DispatchToken::From_SIZE_T(dispatchToken).GetHash();

        size_t mtHash = (size_t)pMT;
        mtHash = (((mtHash >> DISPATCH_CACHE_BITS) + mtHash) >> LOG2_PTRSIZE) & DISPATCH_CACHE_MASK;

Review comment (bug):
The ReclaimDeadEntries method reads m_pDeadList without synchronization before clearing it. While this is called during GC sync point when no other threads are running (as documented), the assignment m_pDeadList = nullptr on line 97 should use a volatile write or InterlockedExchangeT for consistency with how m_pDeadList is updated elsewhere via InterlockedCompareExchangeT. This ensures proper memory ordering and makes the code more robust if calling contexts change in the future.

Code after:
        if (pNewEntry == nullptr)
            return;

        // CAS has release semantics, so the fields of the entry have correct
        // values once the entry is published. If CAS succeeds, we own the old
        // entry for freeing
        InterpDispatchCacheEntry* pOldEntry = InterlockedExchangeT(&m_cache[idx], pNewEntry);

        if (pOldEntry != nullptr)
            AddToDeadList(pOldEntry);
    }

    // Called during GC sync point to free dead entries
    // At this point, no other threads are running, so it is safe to free
    void ReclaimDeadEntries()
    {
        LIMITED_METHOD_CONTRACT;

        InterpDispatchCacheEntry* pDeadList = VolatileLoadWithoutBarrier(&m_pDeadList);

        while (pDeadList != nullptr)
        {
            InterpDispatchCacheEntry* pNext = pDeadList->pNextDead;
            delete pDeadList;
            pDeadList = pNext;
        }

        VolatileStoreWithoutBarrier(&m_pDeadList, (InterpDispatchCacheEntry*)nullptr);
    }

    // Add an entry to the dead list for later cleanup
    void AddToDeadList(InterpDispatchCacheEntry* pEntry)
    {
        LIMITED_METHOD_CONTRACT;

        InterpDispatchCacheEntry* pOldHead;
        do
        {
            pOldHead = VolatileLoadWithoutBarrier(&m_pDeadList);
            pEntry->pNextDead = pOldHead;
        }
        while (InterlockedCompareExchangeT(&m_pDeadList, pEntry, pOldHead) != pOldHead);
    }

    // Same as VSD DispatchCache's HashToken + HashMT
    static uint16_t Hash(size_t dispatchToken, void* pMT, uint16_t dispatchTokenHash)
    {
        LIMITED_METHOD_CONTRACT;

        size_t mtHash = (size_t)pMT;
        mtHash = (((mtHash >> DISPATCH_CACHE_BITS) + mtHash) >> LOG2_PTRSIZE) & DISPATCH_CACHE_MASK;