3636 #include < sanitizer/asan_interface.h>
3737#endif
3838
39+ #ifdef __SANITIZE_THREAD__
40+ #include < sanitizer/tsan_interface.h>
41+ #endif
42+
3943LinearAllocator::LinearAllocator (size_t chunk_size) {
4044 _chunk_size = chunk_size;
4145 _reserve = _tail = allocateChunk (NULL );
@@ -47,9 +51,21 @@ LinearAllocator::~LinearAllocator() {
4751}
4852
4953void LinearAllocator::clear () {
54+ // OS::safeAlloc/safeFree use raw syscalls not intercepted by TSan, so TSan
55+ // never clears shadow memory on munmap. Add explicit acquire/release around
56+ // every plain prev-field read so the happens-before chain from freeChunk's
57+ // __tsan_release reaches any thread that later reuses the same VA.
58+ #ifdef __SANITIZE_THREAD__
59+ __tsan_acquire (_reserve);
60+ #endif
5061 if (_reserve->prev == _tail) {
51- freeChunk (_reserve);
62+ freeChunk (_reserve); // __tsan_release inside
5263 }
64+ #ifdef __SANITIZE_THREAD__
65+ else {
66+ __tsan_release (_reserve); // not freed here; release for future VA-reuse acquirers
67+ }
68+ #endif
5369
5470 // ASAN POISONING: Mark all allocated memory as poisoned BEFORE freeing chunks
5571 // This catches use-after-free even when memory isn't munmap'd (kept in _tail)
@@ -71,12 +87,26 @@ void LinearAllocator::clear() {
7187 }
7288 #endif
7389
74- while (_tail->prev != NULL ) {
90+ // Walk the chain freeing all chunks except the last (prev==NULL).
91+ // Acquire each chunk BEFORE reading its prev field so the TSan happens-before
92+ // chain covers the condition check, not just the assignment inside the body.
93+ {
7594 Chunk *current = _tail;
76- _tail = _tail->prev ;
77- freeChunk (current);
95+ #ifdef __SANITIZE_THREAD__
96+ __tsan_acquire (current); // before the first current->prev read (loop condition)
97+ #endif
98+ while (current->prev != NULL ) {
99+ Chunk *next = current->prev ;
100+ freeChunk (current); // __tsan_release(current) + safeFree inside
101+ current = next;
102+ #ifdef __SANITIZE_THREAD__
103+ __tsan_acquire (current); // before the next iteration's current->prev read
104+ #endif
105+ }
106+ // current is the last chunk (prev==NULL); keep it as the new allocator base.
107+ _reserve = current;
108+ _tail = current;
78109 }
79- _reserve = _tail;
80110 _tail->offs = sizeof (Chunk);
81111
82112 // DON'T UNPOISON HERE - let alloc() do it on-demand!
@@ -88,11 +118,20 @@ ChunkList LinearAllocator::detachChunks() {
88118 // Capture current state before detaching
89119 ChunkList result (_tail, _chunk_size);
90120
91- // Handle reserve chunk: if it's ahead of tail, it needs special handling
92- if (_reserve->prev == _tail) {
93- // Reserve is a separate chunk ahead of tail - it becomes part of detached list
94- // We need to include it in the chain by making it the new head
95- result.head = _reserve;
121+ // Handle reserve chunk: if it's ahead of tail, it becomes part of detached list.
122+ // Acquire TSan ownership before reading _reserve->prev: the reserve chunk may
123+ // have been allocated by another thread via reserveChunk() → allocateChunk(),
124+ // which released ownership with __tsan_release after writing chunk->prev.
125+ if (_reserve != _tail) {
126+ #ifdef __SANITIZE_THREAD__
127+ __tsan_acquire (_reserve);
128+ #endif
129+ if (_reserve->prev == _tail) {
130+ result.head = _reserve;
131+ }
132+ #ifdef __SANITIZE_THREAD__
133+ __tsan_release (_reserve);
134+ #endif
96135 }
97136
98137 // Allocate a fresh chunk for new allocations
@@ -118,17 +157,25 @@ void LinearAllocator::freeChunks(ChunkList& chunks) {
118157 return ;
119158 }
120159
121- // Walk the chain and free each chunk
122160 Chunk* current = chunks.head ;
123161 while (current != nullptr ) {
162+ // Acquire TSan ownership before reading chunk->prev: pairs with the
163+ // __tsan_release in allocateChunk() that published the initialized chunk.
164+ // Without this, TSan cannot connect the writer's (e.g. reserveChunk thread)
165+ // initialization of chunk->prev to this read, and reports a false data race.
166+ #ifdef __SANITIZE_THREAD__
167+ __tsan_acquire (current);
168+ #endif
124169 Chunk* prev = current->prev ;
170+ #ifdef __SANITIZE_THREAD__
171+ __tsan_release (current);
172+ #endif
125173 OS::safeFree (current, chunks.chunk_size );
126174 Counters::decrement (LINEAR_ALLOCATOR_BYTES , chunks.chunk_size );
127175 Counters::decrement (LINEAR_ALLOCATOR_CHUNKS );
128176 current = prev;
129177 }
130178
131- // Mark as freed to prevent double-free
132179 chunks.head = nullptr ;
133180 chunks.chunk_size = 0 ;
134181}
@@ -172,16 +219,28 @@ void *LinearAllocator::alloc(size_t size) {
172219Chunk *LinearAllocator::allocateChunk (Chunk *current) {
173220 Chunk *chunk = (Chunk *)OS::safeAlloc (_chunk_size);
174221 if (chunk != NULL ) {
222+ // OS::safeAlloc uses a raw mmap syscall that bypasses ASan and TSan
223+ // interceptors by design (to avoid profiling self-instrumentation).
224+ // When the OS reuses a VA that had stale sanitizer state from a previous
225+ // allocation at that address, writing to the chunk header triggers:
226+ // ASan: use-after-poison (f7 shadow from prior ASAN_POISON_MEMORY_REGION)
227+ // TSan: data-race (prior access history for the same VA not cleared by munmap)
228+ // Fix: unpoison the entire chunk and acquire TSan ownership BEFORE the first
229+ // write, establishing a clean sanitizer baseline for this logical allocation.
230+ #ifdef ASAN_ENABLED
231+ ASAN_UNPOISON_MEMORY_REGION (chunk, _chunk_size);
232+ #endif
233+ #ifdef __SANITIZE_THREAD__
234+ __tsan_acquire (chunk);
235+ #endif
236+
175237 chunk->prev = current;
176238 chunk->offs = sizeof (Chunk);
177239
178- // ASAN UNPOISONING: New chunks from mmap are clean, unpoison them for use
179- // mmap returns memory that ASan may track as unallocated, so we need to
180- // explicitly unpoison it to allow allocations
181- #ifdef ASAN_ENABLED
182- size_t usable_size = _chunk_size - sizeof (Chunk);
183- void * data_start = (char *)chunk + sizeof (Chunk);
184- ASAN_UNPOISON_MEMORY_REGION (data_start, usable_size);
240+ // Publish the initialized chunk: release TSan ownership so that any thread
241+ // which later acquires this chunk (via __tsan_acquire) will see these writes.
242+ #ifdef __SANITIZE_THREAD__
243+ __tsan_release (chunk);
185244 #endif
186245
187246 Counters::increment (LINEAR_ALLOCATOR_BYTES , _chunk_size);
@@ -191,6 +250,13 @@ Chunk *LinearAllocator::allocateChunk(Chunk *current) {
191250}
192251
193252void LinearAllocator::freeChunk (Chunk *current) {
253+ // Release TSan ownership before munmap so the sanitizer knows this thread is
254+ // done with the memory. The paired __tsan_acquire in allocateChunk() ensures
255+ // the next thread to receive this VA (after OS VA reuse) starts with a clean
256+ // happens-before baseline rather than seeing stale access history.
257+ #ifdef __SANITIZE_THREAD__
258+ __tsan_release (current);
259+ #endif
194260 OS::safeFree (current, _chunk_size);
195261 Counters::decrement (LINEAR_ALLOCATOR_BYTES , _chunk_size);
196262 Counters::decrement (LINEAR_ALLOCATOR_CHUNKS );
@@ -206,7 +272,9 @@ void LinearAllocator::reserveChunk(Chunk *current) {
206272}
207273
208274Chunk *LinearAllocator::getNextChunk (Chunk *current) {
209- Chunk *reserve = _reserve;
275+ // _reserve is written via CAS in reserveChunk(); load it atomically so TSan
276+ // sees the acquire-release relationship with the CAS store.
277+ Chunk *reserve = __atomic_load_n (&_reserve, __ATOMIC_ACQUIRE);
210278
211279 if (reserve == current) {
212280 // Unlikely case: no reserve yet.
0 commit comments