Skip to content

[release/10.0] Fix race condition in cached interface dispatch stubs#126690

Open
github-actions[bot] wants to merge 2 commits intorelease/10.0from
backport/pr-126471-to-release/10.0
Open

[release/10.0] Fix race condition in cached interface dispatch stubs#126690
github-actions[bot] wants to merge 2 commits intorelease/10.0from
backport/pr-126471-to-release/10.0

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot commented Apr 9, 2026

Backport of #126471 to release/10.0

/cc @MichalStrehovsky

Customer Impact

  • Customer reported
  • Found internally

#121632. Race condition in interface dispatch can cause a crash. Somewhat related to #126689.

The cached interface dispatch scheme operates with a cache that has a method pointer in the first field and method-pointer-specific data in the rest of the cache cell. The interface call sequence is:

load dispatch cell address in a non-argument register
do a call to the address obtained by dereferencing the non-argument register

A race condition window exists when the dispatch cell is updated. We can end up with a mismatch where the method we just called expects a different shape of the cache (the cache is a for a different function).

Regression

  • Yes
  • No

Not a regression, this bug existed since first release of .NET Native for UWP in 2015 or so.

Testing

This is a race condition that requires a lot of luck to hit. Testing is "code review" basically.

Risk

IMPORTANT: If this backport is for a servicing release, please verify that:

  • For .NET 8 and .NET 9: The PR target branch is release/X.0-staging, not release/X.0.
  • For .NET 10+: The PR target branch is release/X.0 (no -staging suffix).

Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.

MichalStrehovsky and others added 2 commits April 9, 2026 04:09
The dispatch cell has two fields (m_pStub, m_pCache) that are updated
atomically together but read non-atomically by the caller and stub.
A race during cache growth can cause a stub to read a stale m_pCache
that doesn't match the expected cache size, reading past the cache
allocation into unmapped memory.

Add validation in each RhpInterfaceDispatchN stub: after loading
m_pCache, check that the low 2 bits are zero (not an encoded interface
pointer) and that m_cEntries matches the expected entry count. On
mismatch, re-dispatch through the indirection cell to retry with the
current stub and cache pair.

Fix covers all architectures: amd64, arm64, arm, i386, loongarch64,
and riscv64 (both MASM and GAS variants where applicable).

Also adds OFFSETOF__InterfaceDispatchCache__m_cEntries and
IDC_CACHE_POINTER_MASK constants to CoreCLR's asmconstants.h for
amd64 and arm64 (NativeAOT already had them via AsmOffsets.h).

Fixes #121632

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The same race that affects RhpInterfaceDispatchN stubs can also
affect RhpVTableOffsetDispatch (CoreCLR-only): during the initial
cell transition from RhpInitialInterfaceDispatch to
RhpVTableOffsetDispatch, a concurrent reader could see the new
stub but a stale m_pCache (encoded interface pointer with low bits
set) instead of the expected vtable offset.

Save the cell address in a scratch register (r10 on amd64, x12
on arm64) before overwriting it with m_pCache, then validate the
low bits. On mismatch, re-dispatch through the indirection cell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant