Fix broken critical section in static World create/destroy - #309
Merged
Conversation
penspanic
force-pushed
the
pr/static-worlds-lock
branch
from
July 14, 2026 04:40
d10c677 to
de84389
Compare
World.Create and World.Dispose locked the static Worlds array itself, but Create replaces that array on resize. After a resize, a creator blocked on the old array's monitor and a newly arriving creator locking the new array enter the "critical section" concurrently: two worlds can be handed the same id (cross-wiring EntityInfo lookups into AccessViolation deep in Chunk), and a slot write can land in the stale array so the live Worlds array holds null for a created world (NullReferenceException in EntityExtensions/World.Get right after Create). Readers also indexed Worlds with no publication barrier, so on weakly-ordered CPUs (ARM64) a resized array could become visible before its copied contents. Guard create/destroy with a dedicated lock object, fill the resized copy before publishing it with a volatile write, and write slots volatile. Readers stay lock-free: element loads carry an address dependency on the array reference, so release-publication is sufficient. Adds WorldConcurrencyTest covering concurrent create (forcing resizes) and create/use/destroy churn — both fail against the previous locking in 3/3 runs. WorldRecycle now drains the recycled-id queue first so it no longer depends on the queue being empty when the fixture runs. Co-Authored-By: Claude Fable 5 <[email protected]>
penspanic
force-pushed
the
pr/static-worlds-lock
branch
from
July 14, 2026 04:45
de84389 to
583177b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
World.CreateandWorld.Disposelock the staticWorldsarray itself, butCreatereplaces that array on resize:After a resize, a creator blocked on the old array's monitor and a newly
arriving creator locking the new array enter the critical section
concurrently. Two failure modes follow:
EntityInfolookups cross-wire → fatalAccessViolationExceptiondeep inChunk.GetArrayWorldsarray holdsnullfor a freshly created world →NullReferenceExceptioninEntityExtensions.Get/World.Getimmediately afterCreateReaders (
EntityExtensions, the generated accessors) also indexWorldswithno publication barrier, so on weakly-ordered CPUs (ARM64) a resized array can
become visible before its copied contents.
We hit this constantly in CI (12 of 15 recent failed runs on our project) with
NUnit fixtures creating worlds in parallel; the victim test differed every run,
and about a third of the failures escalated to a fatal AV that killed the test
host.
Fix
WorldsLockobject whose identitynever changes.
array with
Volatile.Write; non-resize slot writes are volatile elementwrites.
dependency on the array reference, so release-publication is sufficient.
No hot-path cost.
Tests
ConcurrentWorldCreateProducesUniqueUsableWorlds— 8 threads × 64 worldscreated concurrently (forcing several resizes), each world used immediately
through the static lookup, then id-uniqueness and slot identity asserted.
ConcurrentWorldCreateDestroyChurnDoesNotCorruptStaticStorage— 8 threads ×200 create/use/destroy rounds so id recycling, slot writes and resizes
interleave.
WorldRecyclenow drains the recycled-id queue before asserting reuse, so itno longer depends on the queue being empty when the fixture runs.
Both new tests fail against the current locking in 3/3 runs (NRE / duplicate
ids) and pass with the fix. The test file is
#if !PURE_ECS— the staticregistry it exercises does not exist under PURE_ECS. Full suite green
(net8.0, Debug + Release × Default / Events / PureECS).