https://github.com/python/cpython/commit/9d0999133e546b36d51469c2738215281571bb9e
commit: 9d0999133e546b36d51469c2738215281571bb9e
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-06-11T11:30:24+03:00
summary:

[3.15] gh-151295: Fix use-after-free in bytes.join()/bytearray.join() via 
re-entrant __buffer__ (GH-151296) (GH-151304)

(cherry picked from commit 84a322aa1555df745a2c115df03bfb7a4ed8c594)

Co-authored-by: tonghuaroot (童话) <[email protected]>

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]

Reply via email to