Skip to content

Commit 05d80cc

Browse files
gh-158803: Fix crash in bytes.join() on a concurrently mutated list (#158910)
In the free-threaded build, bytes.join() and bytearray.join() read items from the list with borrowed references and without holding its lock, so another thread could replace and free an item before it was increfed. Run the join under Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST, as PyUnicode_Join() already does.
1 parent eb77b4b commit 05d80cc

3 files changed

Lines changed: 47 additions & 14 deletions

File tree

‎Lib/test/test_free_threading/test_bytes_object.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from threading import Thread, Barrier
2+
from threading import Thread, Barrier, Event
33
from test.support import threading_helper
44

55
threading_helper.requires_working_threading(module=True)
@@ -32,6 +32,28 @@ def work(ii):
3232

3333
barrier.reset()
3434

35+
def test_racing_join_replace(self):
36+
# gh-158803: join() must not use a list item that another thread
37+
# replaces (and frees) concurrently.
38+
lst = [bytes(10) for _ in range(100)]
39+
done = Event()
40+
41+
def writer():
42+
try:
43+
for _ in range(100):
44+
for i in range(len(lst)):
45+
lst[i] = bytearray(10) if i % 2 else bytes(10)
46+
finally:
47+
done.set()
48+
49+
def reader():
50+
while not done.is_set():
51+
b''.join(lst)
52+
b'-'.join(lst)
53+
bytearray().join(lst)
54+
55+
threading_helper.run_concurrently([writer] + [reader] * 4)
56+
3557

3658
if __name__ == "__main__":
3759
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a crash in :meth:`bytes.join` and :meth:`bytearray.join` in the
2+
:term:`free-threaded build` when another thread concurrently mutates the
3+
list being joined. Patch by Christian Aurich Zanettini Martins.

‎Objects/stringlib/join.h‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
#endif
66

77
Py_LOCAL_INLINE(PyObject *)
8-
STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
8+
STRINGLIB(bytes_join_lock_held)(PyObject *sep, PyObject *seq)
99
{
1010
const char *sepstr = STRINGLIB_STR(sep);
1111
Py_ssize_t seplen = STRINGLIB_LEN(sep);
@@ -14,38 +14,29 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
1414
Py_ssize_t seqlen = 0;
1515
Py_ssize_t sz = 0;
1616
Py_ssize_t i, nbufs;
17-
PyObject *seq, *item;
17+
PyObject *item;
1818
Py_buffer *buffers = NULL;
1919
#define NB_STATIC_BUFFERS 10
2020
Py_buffer static_buffers[NB_STATIC_BUFFERS];
2121
#define GIL_THRESHOLD 1048576
2222
int drop_gil = 1;
2323
PyThreadState *save = NULL;
2424

25-
seq = PySequence_Fast(iterable, "can only join an iterable");
26-
if (seq == NULL) {
27-
return NULL;
28-
}
29-
3025
seqlen = PySequence_Fast_GET_SIZE(seq);
3126
if (seqlen == 0) {
32-
Py_DECREF(seq);
3327
return STRINGLIB_NEW(NULL, 0);
3428
}
3529
#if !STRINGLIB_MUTABLE
3630
if (seqlen == 1) {
3731
item = PySequence_Fast_GET_ITEM(seq, 0);
3832
if (STRINGLIB_CHECK_EXACT(item)) {
39-
Py_INCREF(item);
40-
Py_DECREF(seq);
41-
return item;
33+
return Py_NewRef(item);
4234
}
4335
}
4436
#endif
4537
if (seqlen > NB_STATIC_BUFFERS) {
4638
buffers = PyMem_NEW(Py_buffer, seqlen);
4739
if (buffers == NULL) {
48-
Py_DECREF(seq);
4940
PyErr_NoMemory();
5041
return NULL;
5142
}
@@ -157,13 +148,30 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
157148
error:
158149
res = NULL;
159150
done:
160-
Py_DECREF(seq);
161151
for (i = 0; i < nbufs; i++)
162152
PyBuffer_Release(&buffers[i]);
163153
if (buffers != static_buffers)
164154
PyMem_Free(buffers);
165155
return res;
166156
}
167157

158+
Py_LOCAL_INLINE(PyObject *)
159+
STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
160+
{
161+
PyObject *seq, *res;
162+
163+
seq = PySequence_Fast(iterable, "can only join an iterable");
164+
if (seq == NULL) {
165+
return NULL;
166+
}
167+
168+
Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(iterable);
169+
res = STRINGLIB(bytes_join_lock_held)(sep, seq);
170+
Py_END_CRITICAL_SECTION_SEQUENCE_FAST();
171+
172+
Py_DECREF(seq);
173+
return res;
174+
}
175+
168176
#undef NB_STATIC_BUFFERS
169177
#undef GIL_THRESHOLD

0 commit comments

Comments
 (0)