diff --git a/Lib/test/test_free_threading/test_bytes_object.py b/Lib/test/test_free_threading/test_bytes_object.py index a371e3d533a2cb..a9017bac6de029 100644 --- a/Lib/test/test_free_threading/test_bytes_object.py +++ b/Lib/test/test_free_threading/test_bytes_object.py @@ -1,5 +1,5 @@ import unittest -from threading import Thread, Barrier +from threading import Thread, Barrier, Event from test.support import threading_helper threading_helper.requires_working_threading(module=True) @@ -32,6 +32,28 @@ def work(ii): barrier.reset() + def test_racing_join_replace(self): + # gh-158803: join() must not use a list item that another thread + # replaces (and frees) concurrently. + lst = [bytes(10) for _ in range(100)] + done = Event() + + def writer(): + try: + for _ in range(100): + for i in range(len(lst)): + lst[i] = bytearray(10) if i % 2 else bytes(10) + finally: + done.set() + + def reader(): + while not done.is_set(): + b''.join(lst) + b'-'.join(lst) + bytearray().join(lst) + + threading_helper.run_concurrently([writer] + [reader] * 4) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst new file mode 100644 index 00000000000000..8c95d4882f7863 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst @@ -0,0 +1,3 @@ +Fix a crash in :meth:`bytes.join` and :meth:`bytearray.join` in the +:term:`free-threaded build` when another thread concurrently mutates the +list being joined. Patch by Christian Aurich Zanettini Martins. diff --git a/Objects/stringlib/join.h b/Objects/stringlib/join.h index 5fd2ca70f98dd3..4764baac181b2a 100644 --- a/Objects/stringlib/join.h +++ b/Objects/stringlib/join.h @@ -5,7 +5,7 @@ #endif Py_LOCAL_INLINE(PyObject *) -STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) +STRINGLIB(bytes_join_lock_held)(PyObject *sep, PyObject *seq) { const char *sepstr = STRINGLIB_STR(sep); Py_ssize_t seplen = STRINGLIB_LEN(sep); @@ -14,7 +14,7 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) Py_ssize_t seqlen = 0; Py_ssize_t sz = 0; Py_ssize_t i, nbufs; - PyObject *seq, *item; + PyObject *item; Py_buffer *buffers = NULL; #define NB_STATIC_BUFFERS 10 Py_buffer static_buffers[NB_STATIC_BUFFERS]; @@ -22,30 +22,21 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) int drop_gil = 1; PyThreadState *save = NULL; - seq = PySequence_Fast(iterable, "can only join an iterable"); - if (seq == NULL) { - return NULL; - } - seqlen = PySequence_Fast_GET_SIZE(seq); if (seqlen == 0) { - Py_DECREF(seq); return STRINGLIB_NEW(NULL, 0); } #if !STRINGLIB_MUTABLE if (seqlen == 1) { item = PySequence_Fast_GET_ITEM(seq, 0); if (STRINGLIB_CHECK_EXACT(item)) { - Py_INCREF(item); - Py_DECREF(seq); - return item; + return Py_NewRef(item); } } #endif if (seqlen > NB_STATIC_BUFFERS) { buffers = PyMem_NEW(Py_buffer, seqlen); if (buffers == NULL) { - Py_DECREF(seq); PyErr_NoMemory(); return NULL; } @@ -157,7 +148,6 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) error: res = NULL; done: - Py_DECREF(seq); for (i = 0; i < nbufs; i++) PyBuffer_Release(&buffers[i]); if (buffers != static_buffers) @@ -165,5 +155,23 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) return res; } +Py_LOCAL_INLINE(PyObject *) +STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable) +{ + PyObject *seq, *res; + + seq = PySequence_Fast(iterable, "can only join an iterable"); + if (seq == NULL) { + return NULL; + } + + Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(iterable); + res = STRINGLIB(bytes_join_lock_held)(sep, seq); + Py_END_CRITICAL_SECTION_SEQUENCE_FAST(); + + Py_DECREF(seq); + return res; +} + #undef NB_STATIC_BUFFERS #undef GIL_THRESHOLD