https://github.com/python/cpython/commit/98e55d70bcb7f02c6f98f90739e217049d6a1db7
commit: 98e55d70bcb7f02c6f98f90739e217049d6a1db7
branch: main
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2026-01-06T15:55:37-05:00
summary:
gh-132070: Fix PyObject_Realloc thread-safety in free threaded Python
(gh-143441)
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`).
files:
M Objects/obmalloc.c
M Tools/tsan/suppressions_free_threading.txt
diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c
index c4ccc9e283feb3..b24723f16cf43d 100644
--- a/Objects/obmalloc.c
+++ b/Objects/obmalloc.c
@@ -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
diff --git a/Tools/tsan/suppressions_free_threading.txt
b/Tools/tsan/suppressions_free_threading.txt
index a3e1e54284f0ae..581e9ef26f3c61 100644
--- a/Tools/tsan/suppressions_free_threading.txt
+++ b/Tools/tsan/suppressions_free_threading.txt
@@ -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
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]