https://github.com/python/cpython/commit/05d80ccfe77a781667d1b9e25247c193559cd7cc
commit: 05d80ccfe77a781667d1b9e25247c193559cd7cc
branch: main
author: Christian Aurich Zanettini Martins <[email protected]>
committer: eendebakpt <[email protected]>
date: 2026-10-08T15:58:00+02:00
summary:
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.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-06-05-12-21.gh-issue-158803.iYTpRQ.rst
M Lib/test/test_free_threading/test_bytes_object.py
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
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
_______________________________________________
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]