You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
CoreCLR lazily caches an interface's GuidInfo by initializing a loader-heap entry and publishing its pointer with
an ordinary store. A concurrent cache reader can observe the pointer without acquiring the payload initialization.
The verified in-process reader is limited to supported Windows COM marshalling after ComWrappers.RegisterForMarshalling; it is not a general Type.GUID read failure.
This report is source-analysis-backed and was not dynamically reproduced. The clearest expected symptom is a
sporadic marshalling exception: LoaderHeap returns zero-initialized memory, so an early read of an explicitly
attributed interface entry obtains GUID_NULL or a partially initialized IID, and QueryInterface normally
returns E_NOINTERFACE. The source does not establish memory corruption or use-after-free. Windows Arm64 is the
most relevant supported target because its weak ordering can expose the missing publication edge directly.
Evidence
src\coreclr\vm\class.h:1443-1455 - GetGuidInfo and SetGuidInfo perform plain pointer accesses with no
acquire/release operation.
src\coreclr\vm\methodtable.cpp:6169-6191,6276-6305 - the non-classic interface path consumes the cached GUID
and classification, while the writer initializes those fields and plain-stores the pointer.
src\coreclr\vm\interopconverter.cpp:12-30,100-108 - the Windows COM path uses the non-classic cache value as
the IID for SafeQueryInterface only after the global ComWrappers bridge returns a wrapper.
src\coreclr\System.Private.CoreLib\src\System\Runtime\InteropServices\ComWrappers.CoreCLR.cs:83-96 and src\libraries\System.Private.CoreLib\src\System\Runtime\InteropServices\ComWrappers.cs:1644-1652 - without a
registered global marshalling instance the bridge returns no wrapper; RegisterForMarshalling supplies it.
src\coreclr\utilcode\loaderheap.cpp:419-423,919-931 - loader-heap allocations have a zero-initialization
invariant, including explicit clearing of reused free-list blocks.
Feasible interleaving
After global ComWrappers registration, thread A performs the first GUID lookup for an already loaded,
nongeneric, COM-visible interface with an explicit nonzero GUID. It receives a zero-initialized GuidInfo,
writes the GUID and classification, and plain-stores the cache pointer.
Thread B concurrently marshals an object as that interface. Its global wrapper creation completes, and its
non-classic GetGuid load observes A's non-null pointer without an acquire operation.
B observes GUID_NULL or a partially visible GUID instead of the completely initialized explicit IID.
B passes that IID to SafeQueryInterface; the source-derived expected Release consequence is a spurious E_NOINTERFACE and managed marshalling exception. No dynamic failure was reproduced in this investigation.
Expected behavior
Observing a non-null m_pGuidInfo must also make every preceding write to that immutable entry's GUID and
generation classification visible before either field is consumed.
Actual behavior
The payload stores and cache pointer store/load are ordinary native accesses. There is no release publication,
acquire consumption, or common lock. On Arm64, the pointer can become visible before the payload; the native source
also lacks a compiler-ordering contract on other architectures.
Suggested fix
Repair both accessors as one lock-free protocol. In non-DAC code, release-store the fully initialized pointer from SetGuidInfo with CoreCLR's VolatileStore, and acquire-load it in GetGuidInfo with VolatileLoad. Preserve the
plain DAC read behind DACCESS_COMPILE, the existing field layout, the single local pointer snapshot, and the rule
that payload fields are immutable after publication.
Every racing writer must initialize all payload bytes before its release store. Every live reader must acquire-load
the same slot before dereferencing the selected entry. Published entries must remain owned by the defining
LoaderAllocator; do not add immediate reclamation. A publish-once CAS could additionally eliminate redundant
first-use allocations, but that broader ownership and first-writer-wins change is not required for this repair.
Safety considerations
The selected change adds no lock, waiting, callback, allocation, exception point, GC-mode transition, or lock-order
edge. It preserves loader-heap lifetime and collectible unload behavior, and it does not individually reclaim
entries that a reader may have observed. The DAC path needs a separate plain accessor because its pointer wrapper
and out-of-process reads do not use the live-process synchronization protocol.
On Windows Arm64, the helpers must lower to release/acquire operations such as STLR/LDAR; a plain load,
volatile-without-barrier load, or address dependency is insufficient. The acquire load is on a COM GUID lookup path
that proceeds to substantially more expensive marshalling and QueryInterface work. A new Crst would add
unnecessary blocking in a cooperative-mode reader and complicate GC suspension, metadata work, and reentrancy.
Zero initialization limits the operational stale-data symptom to zero/partially initialized payload observations;
the entry pointer remains valid for the owning loader's lifetime. No memory corruption, use-after-free, deadlock,
or incompatible-vtable execution was demonstrated.
Suggested validation
Extend the existing process-isolated Windows global ComWrappers tests. Register the global marshalling instance,
have ComputeVtables use literal expected IIDs so it does not warm Type.GUID, and race first-time Type.GUID
and/or typed Marshal.GetComInterfaceForObject calls for cold, public, nongeneric, COM-visible interfaces. Use many
distinct interfaces or fresh processes, assert the exact IID, successful QI and method invocation, correct COM
reference release, and absence of exceptions. Add concurrent publishers, a generated-GUID classification case,
and a no-global-wrapper control.
Stress optimized Windows Arm64 on multiple processors and run Windows x64 compatibility coverage. Inspect optimized
code to confirm release publication and acquire consumption, then run relevant GC-stress and collectible-lifetime
coverage. Instrumentation must not insert ordering between payload initialization and publication. No tests,
builds, codegen inspection, or dynamic reproduction were performed for this draft.
Reviewer synthesis
Both reviews confirmed the missing publication edge, the com bucket, an Arm64-relevant lock-free repair, and no
same-root-cause duplicate; targeted arbitration searches also confirmed no duplicate hit. Reviewer A favored the
minimal release-store/acquire-load repair. Reviewer B preferred an acquire-load plus publish-once CAS and correctly
identified two qualifications adopted here: the reader is Windows-only and requires ComWrappers.RegisterForMarshalling, and LoaderHeap zero initialization makes a spurious E_NOINTERFACE the
primary consequence rather than corruption. The minimal release/acquire repair is selected because it completely
restores immutable publication while preserving current multiple-writer ownership semantics.
Note
This issue draft was generated with GitHub Copilot and requires maintainer review before filing.
CoreCLR lazily caches an interface's
GuidInfoby initializing a loader-heap entry and publishing its pointer withan ordinary store. A concurrent cache reader can observe the pointer without acquiring the payload initialization.
The verified in-process reader is limited to supported Windows COM marshalling after
ComWrappers.RegisterForMarshalling; it is not a generalType.GUIDread failure.This report is source-analysis-backed and was not dynamically reproduced. The clearest expected symptom is a
sporadic marshalling exception: LoaderHeap returns zero-initialized memory, so an early read of an explicitly
attributed interface entry obtains
GUID_NULLor a partially initialized IID, andQueryInterfacenormallyreturns
E_NOINTERFACE. The source does not establish memory corruption or use-after-free. Windows Arm64 is themost relevant supported target because its weak ordering can expose the missing publication edge directly.
Evidence
src\coreclr\vm\class.h:1443-1455-GetGuidInfoandSetGuidInfoperform plain pointer accesses with noacquire/release operation.
src\coreclr\vm\methodtable.cpp:6169-6191,6276-6305- the non-classic interface path consumes the cached GUIDand classification, while the writer initializes those fields and plain-stores the pointer.
src\coreclr\vm\interopconverter.cpp:12-30,100-108- the Windows COM path uses the non-classic cache value asthe IID for
SafeQueryInterfaceonly after the global ComWrappers bridge returns a wrapper.src\coreclr\System.Private.CoreLib\src\System\Runtime\InteropServices\ComWrappers.CoreCLR.cs:83-96andsrc\libraries\System.Private.CoreLib\src\System\Runtime\InteropServices\ComWrappers.cs:1644-1652- without aregistered global marshalling instance the bridge returns no wrapper;
RegisterForMarshallingsupplies it.src\coreclr\utilcode\loaderheap.cpp:419-423,919-931- loader-heap allocations have a zero-initializationinvariant, including explicit clearing of reused free-list blocks.
Feasible interleaving
nongeneric, COM-visible interface with an explicit nonzero GUID. It receives a zero-initialized
GuidInfo,writes the GUID and classification, and plain-stores the cache pointer.
non-classic
GetGuidload observes A's non-null pointer without an acquire operation.GUID_NULLor a partially visible GUID instead of the completely initialized explicit IID.SafeQueryInterface; the source-derived expected Release consequence is a spuriousE_NOINTERFACEand managed marshalling exception. No dynamic failure was reproduced in this investigation.Expected behavior
Observing a non-null
m_pGuidInfomust also make every preceding write to that immutable entry's GUID andgeneration classification visible before either field is consumed.
Actual behavior
The payload stores and cache pointer store/load are ordinary native accesses. There is no release publication,
acquire consumption, or common lock. On Arm64, the pointer can become visible before the payload; the native source
also lacks a compiler-ordering contract on other architectures.
Suggested fix
Repair both accessors as one lock-free protocol. In non-DAC code, release-store the fully initialized pointer from
SetGuidInfowith CoreCLR'sVolatileStore, and acquire-load it inGetGuidInfowithVolatileLoad. Preserve theplain DAC read behind
DACCESS_COMPILE, the existing field layout, the single local pointer snapshot, and the rulethat payload fields are immutable after publication.
Every racing writer must initialize all payload bytes before its release store. Every live reader must acquire-load
the same slot before dereferencing the selected entry. Published entries must remain owned by the defining
LoaderAllocator; do not add immediate reclamation. A publish-once CAS could additionally eliminate redundant
first-use allocations, but that broader ownership and first-writer-wins change is not required for this repair.
Safety considerations
The selected change adds no lock, waiting, callback, allocation, exception point, GC-mode transition, or lock-order
edge. It preserves loader-heap lifetime and collectible unload behavior, and it does not individually reclaim
entries that a reader may have observed. The DAC path needs a separate plain accessor because its pointer wrapper
and out-of-process reads do not use the live-process synchronization protocol.
On Windows Arm64, the helpers must lower to release/acquire operations such as
STLR/LDAR; a plain load,volatile-without-barrier load, or address dependency is insufficient. The acquire load is on a COM GUID lookup path
that proceeds to substantially more expensive marshalling and
QueryInterfacework. A newCrstwould addunnecessary blocking in a cooperative-mode reader and complicate GC suspension, metadata work, and reentrancy.
Zero initialization limits the operational stale-data symptom to zero/partially initialized payload observations;
the entry pointer remains valid for the owning loader's lifetime. No memory corruption, use-after-free, deadlock,
or incompatible-vtable execution was demonstrated.
Suggested validation
Extend the existing process-isolated Windows global ComWrappers tests. Register the global marshalling instance,
have
ComputeVtablesuse literal expected IIDs so it does not warmType.GUID, and race first-timeType.GUIDand/or typed
Marshal.GetComInterfaceForObjectcalls for cold, public, nongeneric, COM-visible interfaces. Use manydistinct interfaces or fresh processes, assert the exact IID, successful QI and method invocation, correct COM
reference release, and absence of exceptions. Add concurrent publishers, a generated-GUID classification case,
and a no-global-wrapper control.
Stress optimized Windows Arm64 on multiple processors and run Windows x64 compatibility coverage. Inspect optimized
code to confirm release publication and acquire consumption, then run relevant GC-stress and collectible-lifetime
coverage. Instrumentation must not insert ordering between payload initialization and publication. No tests,
builds, codegen inspection, or dynamic reproduction were performed for this draft.
Reviewer synthesis
Both reviews confirmed the missing publication edge, the
combucket, an Arm64-relevant lock-free repair, and nosame-root-cause duplicate; targeted arbitration searches also confirmed no duplicate hit. Reviewer A favored the
minimal release-store/acquire-load repair. Reviewer B preferred an acquire-load plus publish-once CAS and correctly
identified two qualifications adopted here: the reader is Windows-only and requires
ComWrappers.RegisterForMarshalling, and LoaderHeap zero initialization makes a spuriousE_NOINTERFACEtheprimary consequence rather than corruption. The minimal release/acquire repair is selected because it completely
restores immutable publication while preserving current multiple-writer ownership semantics.
Note
This issue draft was generated with GitHub Copilot and requires maintainer review before filing.