Fix generic virtual dispatch hang after multiple AssemblyLoadContext unloads#132859
Conversation
|
Azure Pipelines: 16 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
@copilot check if CastCache needs similar change for consistency. |
Checked both the managed For consistency I applied the same naming in the managed Note This reply was generated by GitHub Copilot. |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Fixes a correctness issue in the CoreLib GenericCache sentinel detection used during cache flushes, preventing post-flush inserts into the shared sentinel table and avoiding stale generic-virtual-dispatch targets surviving collectible AssemblyLoadContext unloads. Adds a regression test that repeatedly load/invokes/unloads a payload assembly and verifies the virtual-function-pointer cache is cleared after each unload.
Changes:
- Update
GenericCache<TKey, TValue>.TrySetto reliably detect the sentinel table via a sharedSENTINEL_TABLE_SIZEconstant andIsSentinelhelper. - Apply the same sentinel-size constant + helper pattern to
CastCachefor consistency (and tighten the constructor assert). - Add a new
Loader/CollectibleAssemblies/GenericVirtualMethodtest with a payload assembly + runner validating cache flush behavior across repeated unloads.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs | Fix sentinel detection to prevent inserts into the flush sentinel table. |
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CastCache.cs | Align sentinel sizing/detection pattern with GenericCache (consistency + assert tightening). |
| src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethod.csproj | New test project enabling process isolation and referencing payload assembly. |
| src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethod.cs | New regression test exercising repeated collectible unloads and validating the dispatch cache flushes. |
| src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethodUnloaded.csproj | New payload (unloaded) assembly project. |
| src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethodUnloaded.cs | Payload types implementing a generic virtual override that triggers the cache path. |
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
b612474 to
bcd9195
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethod.cs:85
- VerifyVirtualDispatchCacheIsEmpty relies on several reflection lookups that can return null if internal implementation details change. As written, this would surface as a NullReferenceException rather than a clear test failure pointing at the missing field/type. Adding explicit assertions for each reflection step makes the failure mode much easier to triage.
object cache = helpersType.GetField("s_virtualFunctionPointerCache", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
Array table = (Array)cache.GetType().GetField("_table", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(cache);
FieldInfo infoField = table.GetType().GetElementType().GetField("_info", BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo versionField = infoField.FieldType.GetField("_version", BindingFlags.NonPublic | BindingFlags.Instance);
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs:149
- IsSentinel duplicates the table layout knowledge ("+ 1" for the aux slot). Since this type already centralizes the managed/native contract in CacheElementCount, using it here keeps the sentinel definition consistent if the layout ever changes and avoids repeating the +1 rule.
return table.Length == SENTINEL_TABLE_SIZE + 1;
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod/GenericVirtualMethod.cs:30
- This test constructs a collectible AssemblyLoadContext and then polls a fixed number of times (10) for unload/collection. Other CollectibleAssemblies tests gate on
PlatformDetection.IsCollectibleAssembliesSupportedand skip under GC stress because fixed-count polling is unreliable there; without those guards this can be flaky or fail on platforms/configurations that don't support collectible ALCs.
[ActiveIssue("https://github.com/dotnet/runtimelab/issues/155: Collectible assemblies", typeof(Utilities), nameof(Utilities.IsNativeAot))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/34072", TestRuntimes.Mono)]
[Fact]
public static void CallGenericVirtualMethodAcrossUnloads()
GenericCache.TrySetdetected the flush sentinel withtable.Length == 2, but the sentinel array is length 3 (2 entries + element 0 used for aux data). The sentinel was therefore never recognized, so after a flush entries were inserted into the shared sentinel table. Since every flush re-installs that same sentinel, stale entries survived flushes and generic-virtual-dispatch targets belonging to unloaded collectibleAssemblyLoadContexts could be returned once native addresses were reused (hang/crash).src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs: addedSENTINEL_TABLE_SIZEconstant and anIsSentinel(table)helper based onCacheElementCount(table), used by both sentinel checks inTrySet; tightened the constructor assert.src/tests/Loader/CollectibleAssemblies/GenericVirtualMethod(payload assembly with the generic virtual method/override from the issue + runner that loads/invokes/unloads 10 times and asserts the virtual-function-pointer cache is empty after each unload)../build.sh clr+libs -lc release -rc checked(exit 0); test fails against unfixed CoreLib (cache entry version 2 after flush, exit 101) and passes with the fix (exit 100); otherLoader/CollectibleAssembliestests still pass.