Skip to content

Fix concurrent hub-construction SIGSEGV: evict Autofac reflection cache on collectible ALC unload#467

Open
rbuergi wants to merge 2 commits into
mainfrom
fix/reflection-cache-alc-eviction
Open

Fix concurrent hub-construction SIGSEGV: evict Autofac reflection cache on collectible ALC unload#467
rbuergi wants to merge 2 commits into
mainfrom
fix/reflection-cache-alc-eviction

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Root cause

The intermittent native-host SIGSEGV (exit 139) in concurrent per-node hub construction — the crash the #447 exit-marker gate unmasked in FutuRe.Test / Hosting.Monolith.Test — was not a lock/serialization problem. (The earlier global SetupModules lock was the wrong fix and is not used.)

NodeAssemblyLoadContext compiles nodes into collectible load contexts (isCollectible: true) and unloads them on recompile / release / teardown. Its Dispose() called Unload() but never evicted Autofac's process-static shared reflection cache (ReflectionCacheSet.Shared). Autofac keys that cache on ConstructorInfo, so after unload a retained key:

  1. roots the collectible context → it can never be collected — a memory leak on every recompile; and
  2. lets a later, unrelated concurrent GetOrAdd during framework hub construction (AddMeshDataSource) whose key hashes into a bucket holding the stale key compare against freed metadataAccessViolationException in ConcurrentDictionary.TryGetValueInternal (the observed crash frame).

Autofac performs exactly this eviction automatically for scopes created via BeginLoadContextLifetimeScope; MeshWeaver manages its collectible node contexts by hand, so it must run the eviction itself.

Fix

  • New MeshWeaver.ServiceProvider/ReflectionCacheEviction.EvictFor(AssemblyLoadContext) — clears every shared-cache entry referencing the context's assemblies.
  • NodeAssemblyLoadContext subscribes Unloading += ReflectionCacheEviction.EvictFor — runs before unload, while the metadata the predicate walks is still valid; static handler ⇒ no self-reference that would defeat collection. No lock, no serialization, no convoy. (Graph reaches ServiceProvider transitively; no new project reference.)

Test (deterministic, not the flaky AVE)

ReflectionCacheEvictionTest is a root-cause repro: a collectible NodeAssemblyLoadContext activated through Autofac must be GC-collectable after unload. It pins ReflectionCacheSet.Shared alive (the cache is WeakReference-backed — otherwise a plain GC collects the whole cache and false-passes). RED pre-fix (context rooted), GREEN post-fix — a permanent regression guard, unlike the ~50% qemu-amd64 crash.

Validation

  • Deterministic test: RED → GREEN across the fix.
  • Graph.Test regression: 532/532 passed.
  • CI-parity Release -warnaserror: Graph.Test and Hosting.Monolith.Test (the crash site) both build with 0 warnings / 0 errors.

🤖 Generated with Claude Code

…he on collectible ALC unload

Root cause: NodeAssemblyLoadContext (collectible, isCollectible:true) unloaded
compiled-node assemblies without evicting Autofac's process-static shared
reflection cache (ReflectionCacheSet.Shared). Autofac keys that cache on
ConstructorInfo, so a retained key (1) rooted the collectible context — it could
never be collected (a memory leak on every recompile) — and (2) let a later,
unrelated CONCURRENT GetOrAdd during framework hub construction (AddMeshDataSource)
compare against the freed metadata → AccessViolationException in
ConcurrentDictionary.TryGetValueInternal. That is the native-host exit=139 the
#447 exit-marker gate unmasked (FutuRe.Test / Hosting.Monolith.Test).

Fix: NodeAssemblyLoadContext subscribes its Unloading event to
ReflectionCacheEviction.EvictFor, which clears every shared-cache entry
referencing the context's assemblies BEFORE unload (while the metadata the
predicate walks is still valid). This mirrors exactly what Autofac performs
automatically for BeginLoadContextLifetimeScope; MeshWeaver manages its
collectible node contexts by hand, so it must run the eviction itself. No lock,
no serialization, no convoy — the earlier global SetupModules lock was the wrong
fix and is not used.

Test: ReflectionCacheEvictionTest is a deterministic root-cause repro (not the
flaky AVE) — a collectible NodeAssemblyLoadContext activated through Autofac must
be GC-collectable after unload. It pins ReflectionCacheSet.Shared alive (it is
WeakReference-backed, else a plain GC collects the whole cache and false-passes).
RED pre-fix (context rooted), GREEN post-fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an intermittent native crash (SIGSEGV / exit 139) during concurrent hub construction by ensuring Autofac’s process-static reflection cache is purged of entries referencing assemblies in a collectible AssemblyLoadContext before the context unloads, preventing both ALC pinning (leak) and stale-metadata key comparisons.

Changes:

  • Add ReflectionCacheEviction.EvictFor(AssemblyLoadContext) to clear Autofac shared reflection-cache entries tied to a soon-to-unload collectible ALC.
  • Hook NodeAssemblyLoadContext.Unloading to run the eviction during unload initiation.
  • Add a deterministic regression test asserting the collectible node ALC is GC-collectable after unload following Autofac activation, plus a “What’s New” entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/MeshWeaver.Graph.Test/ReflectionCacheEvictionTest.cs New deterministic repro/regression test for ALC collection after Autofac activation.
test/MeshWeaver.Graph.Test/MeshWeaver.Graph.Test.csproj Adds Autofac package reference needed for the new test.
src/MeshWeaver.ServiceProvider/ReflectionCacheEviction.cs New helper to evict Autofac shared reflection cache entries for a collectible ALC.
src/MeshWeaver.Graph/Configuration/CompilationCacheService.cs Subscribes NodeAssemblyLoadContext.Unloading to evict Autofac cache prior to unload.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-07-14-node-recompile-stability.md Documents improved node recompile stability and reduced leak/crash risk.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +37 to +41
for (var i = 0; i < 15 && weak.IsAlive; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 0)

889 tests  ±0   880 ✅ +1   4m 21s ⏱️ -32s
  4 suites ±0     9 💤 ±0 
  4 files   ±0     0 ❌  - 1 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 3)

 12 files  ±0   12 suites  ±0   3m 53s ⏱️ -13s
874 tests +1  874 ✅ +1  0 💤 ±0  0 ❌ ±0 
922 runs  +1  922 ✅ +1  0 💤 ±0  0 ❌ ±0 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 5)

1 252 tests  ±0   1 251 ✅ ±0   4m 53s ⏱️ -25s
   13 suites ±0       1 💤 ±0 
   13 files   ±0       0 ❌ ±0 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 4)

1 649 tests  ±0   1 647 ✅ ±0   5m 38s ⏱️ +15s
   12 suites ±0       2 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 1)

813 tests  +317   626 ✅ +316   4m 54s ⏱️ + 3m 22s
 10 suites +  1   187 💤 +  1 
 10 files   +  1     0 ❌ ±  0 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results (shard 2)

1 249 tests  +1   1 146 ✅ +1   5m 1s ⏱️ +16s
   11 suites ±0     103 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

Test Results

   62 files  +  1     62 suites  +1   28m 42s ⏱️ + 2m 42s
6 726 tests +319  6 424 ✅ +319  302 💤 +1  0 ❌  - 1 
6 774 runs  +319  6 472 ✅ +319  302 💤 +1  0 ❌  - 1 

Results for commit c47ae8a. ± Comparison against base commit d01fdcc.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Status from a verification pass (driving this via /pullrequest): leak is fixed, but the SIGSEGV persists — not merging.

  • Leak half verified. The new ReflectionCacheEvictionTest passes deterministically locally — the unloaded NodeAssemblyLoadContext is now GC-collectable, i.e. the stale ConstructorInfo key is evicted from ReflectionCacheSet.Shared. Build is clean under CI flags.
  • Crash not closed. CI run 29307749907 shard 1 still failed with [CI] MeshWeaver.Hosting.Monolith.Test exit=139 (the same concurrent-construction SIGSEGV). So evicting on NodeAssemblyLoadContext.Unloading is necessary but not sufficient.

Reading: consistent with the investigation's note that beyond the ALC keys a genuine Autofac concurrency bug / Reflection.Emit factory stayed un-isolated — so there's a second contributor, a residual eviction window, or (given the ~50% qemu intermittency) the fix reduces-but-doesn't-eliminate. One red run can't distinguish those, but it's definitive the crash can still fire with this fix.

Not merged (green-main gate). Keeping this branch — the leak fix is worth landing once the residual crash cause is also closed. Confirmed independently: crash point is disconnected from any active construction (dump: all threads idle at crash), so it's use-after-free detonating on a later read — dump can't isolate the corrupting op further.

The exit=139 crash in the ALC-heavy native test hosts (Hosting.Monolith.Test /
FutuRe.Test) is a NATIVE use-after-unload during teardown: a process-wide
reflection/serialization cache retains an accessor into a collectible node
AssemblyLoadContext; when a node-hub disposes it unloads that ALC, and a later
background GC dereferences the freed metadata -> SIGSEGV. The post-crash minidump
is a delayed-detection snapshot (the ALC already unloaded, nothing to gcroot), so
it can't name the offending cache. This is the known .NET collectible-ALC hazard
class (dotnet/runtime #1388, #13283) -- the same SHAPE as the Autofac reflection
cache this branch already evicts, but a DIFFERENT cache (that fix is orthogonal to
the segfault).

Diagnostic (NOT a fix), gated OFF by default so zero prod/CI cost:
MESHWEAVER_ALC_UNLOAD_GC_PROBE=1 makes NodeAssemblyLoadContext.Dispose drive a
synchronous full GC immediately after Unload, logging the node name first. A
dangling native pointer then faults AT that unload -- naming the culprit node and
yielding a corruption-time managed-stack dump -- instead of the opaque delayed
crash. GCStress is the classic tool but needs a checked runtime the hosted runner
lacks; forcing the collection is the retail-runtime equivalent for use-after-unload.

Workflow .github/workflows/alc-unload-probe.yml (workflow_dispatch) loops the native
host under the probe, breaks on a signal exit (139/134/132/136), greps the last
ALC_UNLOAD_PROBE line = culprit node, and uploads the dump + logs. Run it to name
the cache, then apply the evict-on-unload pattern to it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants