Skip to content

Commit fec2734

Browse files
vstinnerStanFromIrelandcmaloney
authored
[3.14] gh-156995: Fix _PyBytes_Resize() in Free Threading (#156996) (#157373)
On Free Threading, _PyBytes_Resize(&obj, 1) no longer returns a single byte singleton if the current thread is different than the thread which created the object. Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com> Co-authored-by: Victor Stinner <vstinner@python.org> (cherry picked from commit fd569ea) Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Cody Maloney <cmaloney@users.noreply.github.com>
1 parent af0798e commit fec2734

4 files changed

Lines changed: 56 additions & 27 deletions

File tree

Lib/test/test_bytes.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1507,7 +1507,6 @@ def test_resize(self):
15071507
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
15081508
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
15091509

1510-
15111510
def test_setitem(self):
15121511
def setitem_as_mapping(b, i, val):
15131512
b[i] = val
@@ -2841,5 +2840,18 @@ def resize_stress(ba):
28412840
with threading_helper.start_threads(threads):
28422841
pass
28432842

2843+
@threading_helper.reap_threads
2844+
@threading_helper.requires_working_threading()
2845+
def test_free_threading_bytearray_resize_other_thread(self):
2846+
# Shrinking a bytearray whose buffer another thread owns must not
2847+
# adopt the immortal single-byte bytes object a the buffer.
2848+
ba = bytearray(b'abc')
2849+
thread = threading.Thread(target=ba.resize, args=(1,))
2850+
with threading_helper.start_threads([thread]):
2851+
pass
2852+
ba[0] = ord('X')
2853+
self.assertEqual(ba, bytearray(b'X'))
2854+
self.assertEqual(ord(b'a'), ord('a'))
2855+
28442856
if __name__ == "__main__":
28452857
unittest.main()

Lib/test/test_capi/test_bytes.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import unittest
23
from test.support import import_helper
34

@@ -231,26 +232,41 @@ def test_decodeescape(self):
231232

232233
def test_resize(self):
233234
"""Test _PyBytes_Resize()"""
234-
resize = _testcapi.bytes_resize
235+
_resize = _testcapi.bytes_resize
236+
237+
def resize(obj, size, new):
238+
result = _resize(obj, size, new)
239+
if 1 <= len(result):
240+
if new or size != len(obj):
241+
# gh-156995: Make sure that the result is a fresh object.
242+
# Previously, _PyBytes_Resize(&obj, 1) returned a singleton
243+
# if _PyObject_IsUniquelyReferenced() is false.
244+
self.assertEqual(sys.getrefcount(result), 1)
245+
self.assertFalse(sys._is_immortal(result))
246+
else:
247+
# check that the result is the empty bytes string singleton
248+
self.assertTrue(sys._is_immortal(result))
249+
return result
235250

236251
for new in True, False:
237-
self.assertEqual(resize(b'abc', 0, new), b'')
238-
self.assertEqual(resize(b'abc', 1, new), b'a')
239-
self.assertEqual(resize(b'abc', 2, new), b'ab')
240-
self.assertEqual(resize(b'abc', 3, new), b'abc')
241-
b = resize(b'abc', 4, new)
242-
self.assertEqual(len(b), 4)
243-
self.assertEqual(b[:3], b'abc')
244-
245-
self.assertEqual(resize(b'a', 0, new), b'')
246-
self.assertEqual(resize(b'a', 1, new), b'a')
247-
b = resize(b'a', 2, new)
248-
self.assertEqual(len(b), 2)
249-
self.assertEqual(b[:1], b'a')
250-
251-
self.assertEqual(resize(b'', 0, new), b'')
252-
self.assertEqual(len(resize(b'', 1, new)), 1)
253-
self.assertEqual(len(resize(b'', 2, new)), 2)
252+
with self.subTest(new=new):
253+
self.assertEqual(resize(b'abc', 0, new), b'')
254+
self.assertEqual(resize(b'abc', 1, new), b'a')
255+
self.assertEqual(resize(b'abc', 2, new), b'ab')
256+
self.assertEqual(resize(b'abc', 3, new), b'abc')
257+
b = resize(b'abc', 4, new)
258+
self.assertEqual(len(b), 4)
259+
self.assertEqual(b[:3], b'abc')
260+
261+
self.assertEqual(resize(b'a', 0, new), b'')
262+
self.assertEqual(resize(b'a', 1, new), b'a')
263+
b = resize(b'a', 2, new)
264+
self.assertEqual(len(b), 2)
265+
self.assertEqual(b[:1], b'a')
266+
267+
self.assertEqual(resize(b'', 0, new), b'')
268+
self.assertEqual(len(resize(b'', 1, new)), 1)
269+
self.assertEqual(len(resize(b'', 2, new)), 2)
254270

255271
self.assertRaises(SystemError, resize, b'abc', -1, False)
256272
self.assertRaises(SystemError, resize, bytearray(b'abc'), 3, False)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
On Free Threading, ``_PyBytes_Resize(&obj, 1)`` no longer returns a single byte
2+
singleton is the current thread is different than the thread which created the
3+
object. Patch by Stan Ulbrych.

Objects/bytesobject.c

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3243,14 +3243,12 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
32433243
return 0;
32443244
}
32453245
if (!_PyObject_IsUniquelyReferenced(v)) {
3246-
if (oldsize < newsize) {
3247-
*pv = _PyBytes_FromSize(newsize, 0);
3248-
if (*pv) {
3249-
memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v), oldsize);
3250-
}
3251-
}
3252-
else {
3253-
*pv = PyBytes_FromStringAndSize(PyBytes_AS_STRING(v), newsize);
3246+
// Allocate and then copy so we don't get a shared immortal
3247+
// one-character singleton!
3248+
*pv = _PyBytes_FromSize(newsize, 0);
3249+
if (*pv) {
3250+
memcpy(PyBytes_AS_STRING(*pv), PyBytes_AS_STRING(v),
3251+
Py_MIN(oldsize, newsize));
32543252
}
32553253
Py_DECREF(v);
32563254
return (*pv == NULL) ? -1 : 0;

0 commit comments

Comments
 (0)