Skip to content

Commit 97e8d13

Browse files
committed
gh-129069: Fix listobject.c data races due to memmove
The use of memmove and _Py_memory_repeat were not thread-safe in the free threading build in some cases. In theory, memmove and _Py_memory_repeat can copy byte-by-byte instead of pointer-by-pointer, so concurrent readers could see uninitialized data or tearing. Additionally, we should be using "release" (or stronger) ordering to be compliant with the C11 memory model when copying objects within a list.
1 parent e79c391 commit 97e8d13

File tree

2 files changed

+64
-47
lines changed

2 files changed

+64
-47
lines changed

Objects/listobject.c

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,7 @@ ensure_shared_on_resize(PyListObject *self)
9696
* of the new slots at exit is undefined heap trash; it's the caller's
9797
* responsibility to overwrite them with sane values.
9898
* The number of allocated elements may grow, shrink, or stay the same.
99-
* Failure is impossible if newsize <= self.allocated on entry, although
100-
* that partly relies on an assumption that the system realloc() never
101-
* fails when passed a number of bytes <= the number of bytes last
102-
* allocated (the C standard doesn't guarantee this, but it's hard to
103-
* imagine a realloc implementation where it wouldn't be true).
99+
* Failure is impossible if newsize <= self.allocated on entry.
104100
* Note that self->ob_item may change, and even if newsize is less
105101
* than ob_size on entry.
106102
*/
@@ -145,6 +141,11 @@ list_resize(PyListObject *self, Py_ssize_t newsize)
145141
#ifdef Py_GIL_DISABLED
146142
_PyListArray *array = list_allocate_array(new_allocated);
147143
if (array == NULL) {
144+
if (newsize < allocated) {
145+
// Never fail when shrinking allocations
146+
Py_SET_SIZE(self, newsize);
147+
return 0;
148+
}
148149
PyErr_NoMemory();
149150
return -1;
150151
}
@@ -178,6 +179,11 @@ list_resize(PyListObject *self, Py_ssize_t newsize)
178179
items = NULL;
179180
}
180181
if (items == NULL) {
182+
if (newsize < allocated) {
183+
// Never fail when shrinking allocations
184+
Py_SET_SIZE(self, newsize);
185+
return 0;
186+
}
181187
PyErr_NoMemory();
182188
return -1;
183189
}
@@ -818,8 +824,8 @@ list_repeat_lock_held(PyListObject *a, Py_ssize_t n)
818824
_Py_RefcntAdd(*src, n);
819825
*dest++ = *src++;
820826
}
821-
// TODO: _Py_memory_repeat calls are not safe for shared lists in
822-
// GIL_DISABLED builds. (See issue #129069)
827+
// This list is not yet visible to other threads, so atomic repeat
828+
// is not necessary even in Py_GIL_DISABLED builds.
823829
_Py_memory_repeat((char *)np->ob_item, sizeof(PyObject *)*output_size,
824830
sizeof(PyObject *)*input_size);
825831
}
@@ -882,6 +888,28 @@ list_clear_slot(PyObject *self)
882888
return 0;
883889
}
884890

891+
// Pointer-by-pointer memmove for PyObject** arrays that is safe
892+
// for shared lists in Py_GIL_DISABLED builds.
893+
static void
894+
list_atomic_memmove(PyListObject *a, PyObject **dest, PyObject **src, Py_ssize_t n)
895+
{
896+
#ifndef Py_GIL_DISABLED
897+
memmove(dest, src, n * sizeof(PyObject *));
898+
#else
899+
if (dest < src) {
900+
for (Py_ssize_t i = 0; i != n; i++) {
901+
_Py_atomic_store_ptr_release(&dest[i], src[i]);
902+
}
903+
}
904+
else {
905+
// copy backwards to avoid overwriting src before it's read
906+
for (Py_ssize_t i = n; i != 0; i--) {
907+
_Py_atomic_store_ptr_release(&dest[i - 1], src[i - 1]);
908+
}
909+
}
910+
#endif
911+
}
912+
885913
/* a[ilow:ihigh] = v if v != NULL.
886914
* del a[ilow:ihigh] if v == NULL.
887915
*
@@ -952,27 +980,17 @@ list_ass_slice_lock_held(PyListObject *a, Py_ssize_t ilow, Py_ssize_t ihigh, PyO
952980
}
953981

954982
if (d < 0) { /* Delete -d items */
955-
Py_ssize_t tail;
956-
tail = (Py_SIZE(a) - ihigh) * sizeof(PyObject *);
957-
// TODO: these memmove/memcpy calls are not safe for shared lists in
958-
// GIL_DISABLED builds. (See issue #129069)
959-
memmove(&item[ihigh+d], &item[ihigh], tail);
960-
if (list_resize(a, Py_SIZE(a) + d) < 0) {
961-
memmove(&item[ihigh], &item[ihigh+d], tail);
962-
memcpy(&item[ilow], recycle, s);
963-
goto Error;
964-
}
983+
Py_ssize_t tail = Py_SIZE(a) - ihigh;
984+
list_atomic_memmove(a, &item[ihigh+d], &item[ihigh], tail);
985+
(void)list_resize(a, Py_SIZE(a) + d); // NB: shrinking a list can't fail
965986
item = a->ob_item;
966987
}
967988
else if (d > 0) { /* Insert d items */
968989
k = Py_SIZE(a);
969990
if (list_resize(a, k+d) < 0)
970991
goto Error;
971992
item = a->ob_item;
972-
// TODO: these memmove/memcpy calls are not safe for shared lists in
973-
// GIL_DISABLED builds. (See issue #129069)
974-
memmove(&item[ihigh+d], &item[ihigh],
975-
(k - ihigh)*sizeof(PyObject *));
993+
list_atomic_memmove(a, &item[ihigh+d], &item[ihigh], k - ihigh);
976994
}
977995
for (k = 0; k < n; k++, ilow++) {
978996
PyObject *w = vitem[k];
@@ -1056,10 +1074,25 @@ list_inplace_repeat_lock_held(PyListObject *self, Py_ssize_t n)
10561074
for (Py_ssize_t j = 0; j < input_size; j++) {
10571075
_Py_RefcntAdd(items[j], n-1);
10581076
}
1059-
// TODO: _Py_memory_repeat calls are not safe for shared lists in
1060-
// GIL_DISABLED builds. (See issue #129069)
1077+
#ifndef Py_GIL_DISABLED
10611078
_Py_memory_repeat((char *)items, sizeof(PyObject *)*output_size,
10621079
sizeof(PyObject *)*input_size);
1080+
#else
1081+
if (_Py_IsOwnedByCurrentThread((PyObject *)self) && !_PyObject_GC_IS_SHARED(self)) {
1082+
// No other threads can read this list concurrently, so atomic repeat is not necessary.
1083+
_Py_memory_repeat((char *)items, sizeof(PyObject *)*output_size,
1084+
sizeof(PyObject *)*input_size);
1085+
return 0;
1086+
}
1087+
1088+
PyObject **src = items;
1089+
PyObject **dst = items + input_size;
1090+
Py_ssize_t remaining = output_size - input_size;
1091+
while (remaining > 0) {
1092+
_Py_atomic_store_ptr_release(dst++, *src++);
1093+
remaining--;
1094+
}
1095+
#endif
10631096
return 0;
10641097
}
10651098

@@ -1532,7 +1565,6 @@ list_pop_impl(PyListObject *self, Py_ssize_t index)
15321565
/*[clinic end generated code: output=6bd69dcb3f17eca8 input=c269141068ae4b8f]*/
15331566
{
15341567
PyObject *v;
1535-
int status;
15361568

15371569
if (Py_SIZE(self) == 0) {
15381570
/* Special-case most common failure cause */
@@ -1548,27 +1580,18 @@ list_pop_impl(PyListObject *self, Py_ssize_t index)
15481580

15491581
PyObject **items = self->ob_item;
15501582
v = items[index];
1551-
const Py_ssize_t size_after_pop = Py_SIZE(self) - 1;
1552-
if (size_after_pop == 0) {
1583+
if (Py_SIZE(self) == 1) {
15531584
Py_INCREF(v);
15541585
list_clear(self);
1555-
status = 0;
1556-
}
1557-
else {
1558-
if ((size_after_pop - index) > 0) {
1559-
memmove(&items[index], &items[index+1], (size_after_pop - index) * sizeof(PyObject *));
1560-
}
1561-
status = list_resize(self, size_after_pop);
1586+
return v;
15621587
}
1563-
if (status >= 0) {
1564-
return v; // and v now owns the reference the list had
1565-
}
1566-
else {
1567-
// list resize failed, need to restore
1568-
memmove(&items[index+1], &items[index], (size_after_pop - index)* sizeof(PyObject *));
1569-
items[index] = v;
1570-
return NULL;
1588+
Py_ssize_t size_after_pop = Py_SIZE(self) - 1;
1589+
if (index < size_after_pop) {
1590+
list_atomic_memmove(self, &items[index], &items[index+1],
1591+
size_after_pop - index);
15711592
}
1593+
list_resize(self, size_after_pop); // NB: shrinking a list can't fail
1594+
return v;
15721595
}
15731596

15741597
/* Reverse a slice of a list in place, from lo up to (exclusive) hi. */

Tools/tsan/suppressions_free_threading.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ race_top:write_thread_id
2323
# https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
2424
thread:pthread_create
2525

26-
# List resizing happens through different paths ending in memcpy or memmove
27-
# (for efficiency), which will probably need to rewritten as explicit loops
28-
# of ptr-sized copies to be thread-safe. (Issue #129069)
29-
race:list_ass_slice_lock_held
30-
race:list_inplace_repeat_lock_held
31-
3226
# PyObject_Realloc internally does memcpy which isn't atomic so can race
3327
# with non-locking reads. See #132070
3428
race:PyObject_Realloc

0 commit comments

Comments
 (0)