Skip to content

Keep the mark chunk pop locks exclusive - #1407

Open
Tutez64 wants to merge 1 commit into
HaxeFoundation:masterfrom
Tutez64:bugfix/gc-pop-lock
Open

Tutez64 wants to merge 1 commit into
HaxeFoundation:masterfrom
Tutez64:bugfix/gc-pop-lock

Conversation

@Tutez64

@Tutez64 Tutez64 commented Sep 29, 2026

Copy link
Copy Markdown

#1389 turned the two pop spin locks in GlobalChunks into:

int expected{ 0 };
while(false == processListPopLock.compare_exchange_strong(expected, 1))
{
   // Spin
}

compare_exchange_strong stores the current value in expected when it fails. After the first failed attempt expected is 1, so the next attempt compares the lock against 1 and succeeds while another thread still holds it. The old _hx_atomic_compare_exchange(&lock, 0, 1) compared against 0 every time.

With the lock no longer exclusive, two marking threads can pop processList (or freeList) at once, which is the ABA case the lock exists to prevent: a thread's compare_exchange can move the head onto a chunk another thread has just taken, so a chunk is processed twice or lost. A lost chunk leaves objects marked but never traversed, and whatever only they reference is freed.

We hit this in a game built with HXCPP_GC_GENERATIONAL after updating hxcpp past #1389: intermittent crashes on native access to freed memory (a MovieClip's __children buffer). With HXCPP_GC_CHECK_POINTER the first bad access was Old object access on a field of an object whose own access had just passed the check, i.e. a marked object whose child had not been marked. The crash does not occur with marking on a single thread, nor with hxcpp from before #1389, nor with this change; I could not reproduce it in a standalone stress test, the interleaving it needs being rare outside the game.

The fix resets expected to 0 on every spin, in both loops. mZeroLock and gPauseForCollect use a single attempt and are unaffected.

compare_exchange_strong writes the current value into expected when it fails. The spin loops kept that
value, so the next attempt compared against 1 and succeeded while another thread still held the lock.
Two marking threads could then pop the same chunk list at once, and a chunk could be lost: its objects
were marked but never traversed, and what only they referenced was freed.

Co-Authored-By: Claude Opus 5.5 <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.

1 participant