From 81578b01bce6513b8d018373b2e34d74156c7a2d Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Tue, 6 Oct 2026 08:38:56 -0300 Subject: [PATCH] gh-158803: Fix crash in bytes.join() on a concurrently mutated list 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. --- .../test_free_threading/test_bytes_object.py | 24 ++++++++++++- ...-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst | 3 ++ Objects/stringlib/join.h | 34 ++++++++++++------- 3 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst diff --git a/Lib/test/test_free_threading/test_bytes_object.py b/Lib/test/test_free_threading/test_bytes_object.py index a371e3d533a2cb9..a9017bac6de0295 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 000000000000000..8c95d4882f78631 --- /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 5fd2ca70f98dd31..4764baac181b2ad 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