From ae1701686c272545acda89ca4576ebcc824186ce Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 13 Sep 2026 16:26:45 +0200 Subject: [PATCH] [3.14] gh-156995: Fix _PyBytes_Resize() in Free Threading (#156996) (#157373) _PyBytes_Resize(&obj, 1) no longer returns a single byte singleton if the reference count is greater than 1. (cherry picked from commit fd569ea94120815b01baf8bcbcf21e8e09102db6) Co-authored-by: Stan Ulbrych Co-authored-by: Cody Maloney Co-authored-by: Victor Stinner --- Lib/test/test_capi/test_bytes.py | 54 ++++++++++++------- ...-09-12-21-00-42.gh-issue-156995.UVjqQe.rst | 2 + Objects/bytesobject.c | 13 ++--- 3 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-09-12-21-00-42.gh-issue-156995.UVjqQe.rst diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py index 35b725ae34a1edc..b67948a644e682b 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,43 @@ def test_decodeescape(self): def test_resize(self): """Test _PyBytes_Resize()""" - resize = _testcapi.bytes_resize + _resize = _testcapi.bytes_resize + empty_singleton = bytes() + + 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 the reference count is higher than 1. + # Expect 2 references since sys.getrefcount() call adds + # a reference. + self.assertEqual(sys.getrefcount(result), 2) + else: + # check that the result is the empty bytes string singleton + self.assertIs(result, empty_singleton) + 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 000000000000000..11629eca29bdcf2 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-12-21-00-42.gh-issue-156995.UVjqQe.rst @@ -0,0 +1,2 @@ +``_PyBytes_Resize(&obj, 1)`` no longer returns a single byte singleton if the +reference count is greater than ``1``. Patch by Stan Ulbrych. diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index af6ca7270b007ba..fb5526370a54ab8 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -3192,14 +3192,11 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize) return 0; } if (Py_REFCNT(v) != 1) { - 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 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;