https://github.com/python/cpython/commit/84a322aa1555df745a2c115df03bfb7a4ed8c594
commit: 84a322aa1555df745a2c115df03bfb7a4ed8c594
branch: main
author: tonghuaroot (童话) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-11T07:51:43Z
summary:
gh-151295: Fix use-after-free in bytes.join()/bytearray.join() via re-entrant
__buffer__ (GH-151296)
files:
A Misc/NEWS.d/next/Library/2026-06-11-00-00-00.gh-issue-151295.NQYUzW.rst
M Lib/test/test_bytes.py
M Objects/stringlib/join.h
diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py
index e0e8dd4eccfb1b..e211c3d15a4ed2 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -645,6 +645,32 @@ def test_join(self):
with self.assertRaises(TypeError):
dot_join([memoryview(b"ab"), "cd", b"ef"])
+ def test_join_concurrent_buffer_mutation(self):
+ # __buffer__() can release the GIL, letting another thread concurrently
+ # mutate the joined sequence (simulated here by mutating in
__buffer__).
+ # See: https://github.com/python/cpython/issues/151295
+ def make_seq(mutate):
+ # Item is only referenced from the list slot, so mutate() frees it.
+ class Item:
+ def __buffer__(self, flags):
+ mutate(seq)
+ return memoryview(b'x')
+ seq = [b'a', Item(), b'c']
+ return seq
+
+ for sep in (self.type2test(b''), self.type2test(b'::')):
+ with self.subTest(sep=sep):
+ # Changing the list length is reported as a RuntimeError.
+ seq = make_seq(lambda seq: seq.clear())
+ self.assertRaises(RuntimeError, sep.join, seq)
+
+ # The list length is unchanged, so the size-change recheck
+ # cannot fire: only keeping the item alive avoids the crash.
+ def replace(seq):
+ seq[1] = b'z'
+ seq = make_seq(replace)
+ self.assertEqual(sep.join(seq), sep.join([b'a', b'x', b'c']))
+
def test_count(self):
b = self.type2test(b'mississippi')
i = 105
diff --git
a/Misc/NEWS.d/next/Library/2026-06-11-00-00-00.gh-issue-151295.NQYUzW.rst
b/Misc/NEWS.d/next/Library/2026-06-11-00-00-00.gh-issue-151295.NQYUzW.rst
new file mode 100644
index 00000000000000..e9012f023ff7f7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-11-00-00-00.gh-issue-151295.NQYUzW.rst
@@ -0,0 +1,4 @@
+Fixed a crash (use-after-free) in :meth:`bytes.join` and
+:meth:`bytearray.join` that could occur if an item's
+:meth:`~object.__buffer__` concurrently mutates the sequence being joined.
+The mutation is now reported as a :exc:`RuntimeError` instead.
diff --git a/Objects/stringlib/join.h b/Objects/stringlib/join.h
index de6bd83ffe4c8b..deebfeadc0f4fd 100644
--- a/Objects/stringlib/join.h
+++ b/Objects/stringlib/join.h
@@ -68,13 +68,18 @@ STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
buffers[i].len = PyBytes_GET_SIZE(item);
}
else {
+ /* item is only borrowed; its __buffer__() may run Python that
+ drops the sequence's last reference to it. */
+ Py_INCREF(item);
if (PyObject_GetBuffer(item, &buffers[i], PyBUF_SIMPLE) != 0) {
+ Py_DECREF(item);
PyErr_Format(PyExc_TypeError,
"sequence item %zd: expected a bytes-like object,
"
"%.80s found",
i, Py_TYPE(item)->tp_name);
goto error;
}
+ Py_DECREF(item);
/* If the backing objects are mutable, then dropping the GIL
* opens up race conditions where another thread tries to modify
* the object which we hold a buffer on it. Such code has data
_______________________________________________
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]