diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 7fb274ce427002..d9d65b223f033a 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1507,7 +1507,6 @@ def test_resize(self): self.assertRaises(MemoryError, bytearray().resize, sys.maxsize) self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize) - def test_setitem(self): def setitem_as_mapping(b, i, val): b[i] = val @@ -2841,5 +2840,18 @@ def resize_stress(ba): with threading_helper.start_threads(threads): pass + @threading_helper.reap_threads + @threading_helper.requires_working_threading() + def test_free_threading_bytearray_resize_other_thread(self): + # Shrinking a bytearray whose buffer another thread owns must not + # adopt the immortal single-byte bytes object a the buffer. + ba = bytearray(b'abc') + thread = threading.Thread(target=ba.resize, args=(1,)) + with threading_helper.start_threads([thread]): + pass + ba[0] = ord('X') + self.assertEqual(ba, bytearray(b'X')) + self.assertEqual(ord(b'a'), ord('a')) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py index bc820bd68d9e21..a3e10771b19da7 100644 --- a/Lib/test/test_capi/test_bytes.py +++ b/Lib/test/test_capi/test_bytes.py @@ -1,3 +1,4 @@ +import sys import unittest from test.support import import_helper @@ -231,26 +232,41 @@ def test_decodeescape(self): def test_resize(self): """Test _PyBytes_Resize()""" - resize = _testcapi.bytes_resize + _resize = _testcapi.bytes_resize + + def resize(obj, size, new): + result = _resize(obj, size, new) + if 1 <= len(result): + if new or size != len(obj): + # gh-156995: Make sure that the result is a fresh object. + # Previously, _PyBytes_Resize(&obj, 1) returned a singleton + # if _PyObject_IsUniquelyReferenced() is false. + self.assertEqual(sys.getrefcount(result), 1) + self.assertFalse(sys._is_immortal(result)) + else: + # check that the result is the empty bytes string singleton + self.assertTrue(sys._is_immortal(result)) + return result for new in True, False: - self.assertEqual(resize(b'abc', 0, new), b'') - self.assertEqual(resize(b'abc', 1, new), b'a') - self.assertEqual(resize(b'abc', 2, new), b'ab') - self.assertEqual(resize(b'abc', 3, new), b'abc') - b = resize(b'abc', 4, new) - self.assertEqual(len(b), 4) - self.assertEqual(b[:3], b'abc') - - self.assertEqual(resize(b'a', 0, new), b'') - self.assertEqual(resize(b'a', 1, new), b'a') - b = resize(b'a', 2, new) - self.assertEqual(len(b), 2) - self.assertEqual(b[:1], b'a') - - self.assertEqual(resize(b'', 0, new), b'') - self.assertEqual(len(resize(b'', 1, new)), 1) - self.assertEqual(len(resize(b'', 2, new)), 2) + with self.subTest(new=new): + self.assertEqual(resize(b'abc', 0, new), b'') + self.assertEqual(resize(b'abc', 1, new), b'a') + self.assertEqual(resize(b'abc', 2, new), b'ab') + self.assertEqual(resize(b'abc', 3, new), b'abc') + b = resize(b'abc', 4, new) + self.assertEqual(len(b), 4) + self.assertEqual(b[:3], b'abc') + + self.assertEqual(resize(b'a', 0, new), b'') + self.assertEqual(resize(b'a', 1, new), b'a') + b = resize(b'a', 2, new) + self.assertEqual(len(b), 2) + self.assertEqual(b[:1], b'a') + + self.assertEqual(resize(b'', 0, new), b'') + self.assertEqual(len(resize(b'', 1, new)), 1) + self.assertEqual(len(resize(b'', 2, new)), 2) self.assertRaises(SystemError, resize, b'abc', -1, False) self.assertRaises(SystemError, resize, bytearray(b'abc'), 3, False) diff --git a/Misc/NEWS.d/next/C_API/2026-09-12-21-00-42.gh-issue-156995.UVjqQe.rst b/Misc/NEWS.d/next/C_API/2026-09-12-21-00-42.gh-issue-156995.UVjqQe.rst new file mode 100644 index 00000000000000..f496a9dc8880fe --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-12-21-00-42.gh-issue-156995.UVjqQe.rst @@ -0,0 +1,3 @@ +On Free Threading, ``_PyBytes_Resize(&obj, 1)`` no longer returns a single byte +singleton is the current thread is different than the thread which created the +object. Patch by Stan Ulbrych. diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 03245788bb1040..e2213975254080 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -3243,14 +3243,12 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize) return 0; } if (!_PyObject_IsUniquelyReferenced(v)) { - if (oldsize < newsize) { - *pv = _PyBytes_FromSize(newsize, 0); - if (*pv) { - memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v), oldsize); - } - } - else { - *pv = PyBytes_FromStringAndSize(PyBytes_AS_STRING(v), newsize); + // Allocate and then copy so we don't get a shared immortal + // one-character singleton! + *pv = _PyBytes_FromSize(newsize, 0); + if (*pv) { + memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v), + Py_MIN(oldsize, newsize)); } Py_DECREF(v); return (*pv == NULL) ? -1 : 0;