From a4c76a64e70d26387b355ac5b9fadb6ae0d825fd Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Fri, 2 Jan 2026 11:35:20 -0500 Subject: [PATCH 1/3] gh-132070: Fix PyObject_Realloc thread-safety in free threaded Python 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 | 36 +++++++++++++++++++++- Tools/tsan/suppressions_free_threading.txt | 4 --- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index c4ccc9e283feb3..d0bc3f608d5c76 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -307,8 +307,42 @@ _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 safe access 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*)) { + 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 From da8b157fc9e05f2a79302ca8b449306d22085417 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Mon, 5 Jan 2026 16:21:57 -0500 Subject: [PATCH 2/3] Update Objects/obmalloc.c Co-authored-by: T. Wouters --- Objects/obmalloc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index d0bc3f608d5c76..f4ad3162c78e59 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -320,7 +320,7 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) return NULL; } - // Free threaded Python allows safe access to the PyObject reference count + // 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. From 3387cd2415033c0e367d8303c39103594018949b Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 6 Jan 2026 11:43:12 -0500 Subject: [PATCH 3/3] Add comment about memcpy --- Objects/obmalloc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index f4ad3162c78e59..b24723f16cf43d 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -332,6 +332,9 @@ _PyObject_MiRealloc(void *ctx, void *ptr, size_t nbytes) 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);