From e5d7cb72231f35c24589bdeed3e8422251809083 Mon Sep 17 00:00:00 2001 From: Chris Speciale Date: Thu, 12 Nov 2026 07:00:25 -0500 Subject: [PATCH] Take each block's zero lock in the incremental start-age test hook __hxcpp_gc_incremental_test(10), which IncrementalGcStartSweep calls between collections, read every block's start bits and object headers without a lock. The zeroing thread that a collection starts keeps running while the program does: under each block's mZeroLock it reclaims the blocks a finished cycle left unswept and zeroes their holes, which writes the start bits and rows the hook reads. TSan reported the pair on main twice (#436). It needs the zeroing thread to be on a block while the hook reads it, so most runs passed. The hook now holds each block's mZeroLock while it reads the block, as the incremental sweep (SweepBlocks) does. Nothing outside HXCPP_GC_INCREMENTAL_TEST reads blocks this way. The allocator claims a block through the same lock before it uses it. A collection stops the zeroing thread before it reclaims, moves or cancels a cycle. A slice that leaves it running first reclaims the free blocks under the lock, after which the thread only zeroes holes, which marking does not read. So programs could not hit this, and there is no Changes.md line. Fixes #436 --- src/hx/gc/Immix.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index b530ae8d..cd812097 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -7203,7 +7203,16 @@ class GlobalAllocator { int oldest = 0; for(int i=0;imaxStartAge()); + { + BlockDataInfo *block = mAllBlocks[i]; + // Runs between collections, while the zeroing thread may be + // reclaiming or zeroing the block under this lock + int expected = 0; + while (!block->mZeroLock.compare_exchange_weak(expected, 1)) + expected = 0; + oldest = std::max(oldest, block->maxStartAge()); + block->mZeroLock = 0; + } return oldest; } #endif