[compiler-rt] [Darwin][TSan] Fix false positive race report when using dispatch_apply (PR #204866)

Dan Blackwell via llvm-commits llvm-commits at lists.llvm.org
Fri Jun 26 05:21:35 PDT 2026


https://github.com/DanBlackwell updated https://github.com/llvm/llvm-project/pull/204866

>From c069106254f81bee61e41c49993fe6d08a04082e Mon Sep 17 00:00:00 2001
From: Dan Blackwell <dan_blackwell at apple.com>
Date: Fri, 19 Jun 2026 17:52:29 +0100
Subject: [PATCH] [Darwin][TSan] Fix false positive race report when using
 dispatch_apply

Currently I have seen some false positive race reports caused by instrumented block copy code that writes after the release-annotation. When the block runs on another thread and reads the copied data, it incorrectly reports this as a race.

This patch casts the block to a 'void *' in order to avoid the block copy - thus avoiding the reported race. This is safe because dispatch_apply runs synchronously, so the block lives beyond all invocations.

rdar://92286127
---
 .../rtl/tsan_interceptors_libdispatch.cpp     | 18 ++++++++++-
 .../test/tsan/Darwin/dispatch_apply.mm        | 30 +++++++++++++++++++
 2 files changed, 47 insertions(+), 1 deletion(-)
 create mode 100644 compiler-rt/test/tsan/Darwin/dispatch_apply.mm

diff --git a/compiler-rt/lib/tsan/rtl/tsan_interceptors_libdispatch.cpp b/compiler-rt/lib/tsan/rtl/tsan_interceptors_libdispatch.cpp
index 2104fe7fd059f..64d766c993a86 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interceptors_libdispatch.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_interceptors_libdispatch.cpp
@@ -511,12 +511,28 @@ TSAN_INTERCEPTOR(void, dispatch_apply, size_t iterations,
   uptr parent_to_child_sync = (uptr)&sync1;
   uptr child_to_parent_sync = (uptr)&sync2;
 
+  // Capturing `block` inside `new_block` (below) causes `new_block`'s copy
+  // helper to `_Block_copy` `block`. Any captured objects must be `retain`ed
+  // and their (retained) pointers stored into the heap Block_layout. This
+  // happens on the calling thread inside `REAL(dispatch_apply)` below, after
+  // the `Release(..., parent_to_child_sync)` call.
+  //
+  // When `block` gets invoked in `new_block` on a worker thread, it reads
+  // those same captures. The worker does not see a happens-before edge
+  // between the writes during the copy and these reads, resulting in a
+  // false positive race report.
+  //
+  // We avoid this copy by taking a raw pointer to the block and directly
+  // invoking that. This is safe because `dispatch_apply` will not return
+  // until all iterations have completed - thus `block` will live past the
+  // end of all uses of `block_ptr`.
+  void* block_ptr = (void*)block;
   Release(thr, pc, parent_to_child_sync);
   void (^new_block)(size_t) = ^(size_t iteration) {
     SCOPED_INTERCEPTOR_RAW(dispatch_apply);
     Acquire(thr, pc, parent_to_child_sync);
     SCOPED_TSAN_INTERCEPTOR_USER_CALLBACK_START();
-    block(iteration);
+    ((void (^)(size_t))block_ptr)(iteration);
     SCOPED_TSAN_INTERCEPTOR_USER_CALLBACK_END();
     Release(thr, pc, child_to_parent_sync);
   };
diff --git a/compiler-rt/test/tsan/Darwin/dispatch_apply.mm b/compiler-rt/test/tsan/Darwin/dispatch_apply.mm
new file mode 100644
index 0000000000000..2daa877962ea2
--- /dev/null
+++ b/compiler-rt/test/tsan/Darwin/dispatch_apply.mm
@@ -0,0 +1,30 @@
+// Check that dispatch_apply() does not report a false data race when the
+// dispatched block's copy helper retains the captures into the heap block.
+// NOTE: this test may spuriously pass, but is a best effort to reproduce
+//       the problem, which relies on OS thread scheduling mechanics.
+
+// RUN: %clang_tsan %s -o %t -framework Foundation
+// RUN: %run %t 2>&1 | FileCheck %s
+
+#import <Foundation/Foundation.h>
+#import <dispatch/dispatch.h>
+
+int main() {
+  // Strong capture -> non-trivial copy helper. Enough iterations that
+  // libdispatch fans out to worker threads (captures read off-thread).
+  const size_t n = 1024;
+  NSMutableArray *items = [NSMutableArray array];
+  for (size_t i = 0; i < n; i++)
+    [items addObject:@(i)];
+
+  dispatch_apply(n, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
+                 ^(size_t idx) {
+                   (void)[items objectAtIndex:idx];
+                 });
+
+  NSLog(@"Done.");
+  return 0;
+}
+
+// CHECK-NOT: WARNING: ThreadSanitizer
+// CHECK: Done.



More information about the llvm-commits mailing list