https://github.com/python/cpython/commit/21238ecc1d0131d490573316fa95e07ada6eb5e4
commit: 21238ecc1d0131d490573316fa95e07ada6eb5e4
branch: 3.14
author: Christian Aurich Zanettini Martins <[email protected]>
committer: eendebakpt <[email protected]>
date: 2026-10-09T11:43:05+02:00
summary:
[3.14] gh-158803: Fix crash in bytes.join() on a concurrently mutated list
(#159031)
[3.14] gh-158803: Fix crash in bytes.join() on a concurrently mutated list
(GH-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.
(cherry picked from commit 05d80ccfe77a781667d1b9e25247c193559cd7cc)
files:
A Lib/test/test_free_threading/test_bytes_object.py
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst
M Objects/bytesobject.c
M Objects/stringlib/join.h
diff --git a/Lib/test/test_free_threading/test_bytes_object.py
b/Lib/test/test_free_threading/test_bytes_object.py
new file mode 100644
index 000000000000000..91ee2669010f218
--- /dev/null
+++ b/Lib/test/test_free_threading/test_bytes_object.py
@@ -0,0 +1,33 @@
+import unittest
+from threading import Event
+from test.support import threading_helper
+
+threading_helper.requires_working_threading(module=True)
+
+
+class BytesThreading(unittest.TestCase):
+ 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/bytesobject.c b/Objects/bytesobject.c
index aafbe398417898a..287d1e436fa641b 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -6,6 +6,7 @@
#include "pycore_bytesobject.h" // _PyBytes_Find(), _PyBytes_Repeat()
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_ceval.h" // _PyEval_GetBuiltin()
+#include "pycore_critical_section.h" //
Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST()
#include "pycore_format.h" // F_LJUST
#include "pycore_global_objects.h"// _Py_GET_GLOBAL_OBJECT()
#include "pycore_initconfig.h" // _PyStatus_OK()
diff --git a/Objects/stringlib/join.h b/Objects/stringlib/join.h
index deebfeadc0f4fd8..8bed9b6dda978af 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;
}
@@ -155,7 +146,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)
@@ -163,5 +153,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
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]