]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-132070: Fix PyObject_Realloc thread-safety in free threaded Python (gh-143441)
authorSam Gross <colesbury@gmail.com>
Tue, 6 Jan 2026 20:55:37 +0000 (15:55 -0500)
committerGitHub <noreply@github.com>
Tue, 6 Jan 2026 20:55:37 +0000 (15:55 -0500)
The PyObject header reference count fields must be initialized using
atomic operations because they may be concurrently read by another
thread (e.g., from `_Py_TryIncref`).

Objects/obmalloc.c
Tools/tsan/suppressions_free_threading.txt

index c4ccc9e283feb3e5434fbea5ab1922240f7960aa..b24723f16cf43db2fed89c45594195999dd80eb2 100644 (file)
@@ -307,8 +307,45 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes)
 {
 #ifdef Py_GIL_DISABLED
     _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET();
+    // Implement our own realloc logic so that we can copy PyObject header
+    // in a thread-safe way.
+    size_t size = mi_usable_size(ptr);
+    if (nbytes <= size && nbytes >= (size / 2) && nbytes > 0) {
+        return ptr;
+    }
+
     mi_heap_t *heap = tstate->mimalloc.current_object_heap;
-    return mi_heap_realloc(heap, ptr, nbytes);
+    void* newp = mi_heap_malloc(heap, nbytes);
+    if (newp == NULL) {
+        return NULL;
+    }
+
+    // Free threaded Python allows access from other threads to the PyObject reference count
+    // fields for a period of time after the object is freed (see InternalDocs/qsbr.md).
+    // These fields are typically initialized by PyObject_Init() using relaxed
+    // atomic stores. We need to copy these fields in a thread-safe way here.
+    // We use the "debug_offset" to determine how many bytes to copy -- it
+    // includes the PyObject header and plus any extra pre-headers.
+    size_t offset = heap->debug_offset;
+    assert(offset % sizeof(void*) == 0);
+
+    size_t copy_size = (size < nbytes ? size : nbytes);
+    if (copy_size >= offset) {
+        for (size_t i = 0; i != offset; i += sizeof(void*)) {
+            // Use memcpy to avoid strict-aliasing issues. However, we probably
+            // still have unavoidable strict-aliasing issues with
+            // _Py_atomic_store_ptr_relaxed here.
+            void *word;
+            memcpy(&word, (char*)ptr + i, sizeof(void*));
+            _Py_atomic_store_ptr_relaxed((void**)((char*)newp + i), word);
+        }
+        _mi_memcpy((char*)newp + offset, (char*)ptr + offset, copy_size - offset);
+    }
+    else {
+        _mi_memcpy(newp, ptr, copy_size);
+    }
+    mi_free(ptr);
+    return newp;
 #else
     return mi_realloc(ptr, nbytes);
 #endif
index a3e1e54284f0ae8d9c2492bcdff32e608f1f64ee..581e9ef26f3c61091a62d054238c8bbf846d3f79 100644 (file)
@@ -16,7 +16,3 @@ race_top:_PyObject_TryGetInstanceAttribute
 
 # https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
 thread:pthread_create
-
-# PyObject_Realloc internally does memcpy which isn't atomic so can race
-# with non-locking reads. See #132070
-race:PyObject_Realloc