From 67e6be72be9c0b75a31795ed42f9afb5eb431d47 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 04:24:32 +0200 Subject: [PATCH 1/4] gh-157242: Leave bytearray unchanged if resize() fails (#157340) If bytearray.resize() or bytearray.take_bytes() fails, leave the bytearray unchanged. Add PyBytesWriter_Resize() error test on bytearray. --- Lib/test/test_bytes.py | 65 +++++++++++ Lib/test/test_capi/test_bytes.py | 36 +++--- ...-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst | 3 + Objects/bytearrayobject.c | 103 +++++++++++------- 4 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index e73cd7d5d826bc2..0f21ffb5ecb9b4f 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -5,6 +5,7 @@ """ import array +import contextlib import operator import os import re @@ -48,6 +49,19 @@ def __index__(self): return self.value +@contextlib.contextmanager +def inject_memory_error(testcase, start): + # Raise SkipTest if _testcapi extension module is missing + _testcapi = import_helper.import_module('_testcapi') + + with testcase.assertRaises(MemoryError): + try: + _testcapi.set_nomemory(start) + yield + finally: + _testcapi.remove_mem_hooks() + + class BaseBytesTest: def assertTypedEqual(self, actual, expected): @@ -1555,6 +1569,35 @@ def test_resize(self): self.assertRaises(MemoryError, bytearray().resize, sys.maxsize) self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize) + def test_resize_error(self): + # gh-157242: If bytearray.resize() fails (MemoryError), + # the bytearray must be left unchanged. + + offset = 3 + for logical_offset in (False, True): + with self.subTest(logical_offset=logical_offset): + # grow bytearray + ba = bytearray(b'0123456789') + if logical_offset: + expected = ba[offset:] + del ba[:offset] + else: + expected = ba.copy() + with inject_memory_error(self, 0): + ba.resize(1024) + self.assertEqual(ba, expected) + + # shrink bytearray + ba = bytearray(b'0123456789') + if logical_offset: + expected = ba[offset:] + del ba[:offset] + else: + expected = ba.copy() + with inject_memory_error(self, 0): + ba.resize(1) + self.assertEqual(ba, expected) + def test_take_bytes(self): ba = bytearray(b'ab') self.assertEqual(ba.take_bytes(), b'ab') @@ -1619,6 +1662,28 @@ def test_take_bytes(self): self.assertEqual(ba, bytearray(b'A')) self.assertEqual(ord(b'c'), ord('c')) + def test_take_bytes_error(self): + # gh-157242: If bytearray.take_bytes() fails (MemoryError), + # the bytearray must be left unchanged. + + for logical_offset, to_take, mem_errors in ( + (True, 5, (0, 1)), + (False, 5, (0, 1)), + (True, None, (0,)), + ): + for mem_error in mem_errors: + with self.subTest(logical_offset=logical_offset, + to_take=to_take, mem_error=mem_error): + ba = bytearray(b'0123456789') + if logical_offset: + expected = ba[3:] + del ba[:3] + else: + expected = ba.copy() + with inject_memory_error(self, mem_error): + ba.take_bytes(to_take) + self.assertEqual(ba, expected) + @support.cpython_only # tests an implementation detail def test_take_bytes_optimization(self): # Validate optimization around taking lots of little chunks out of a diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py index fab692b70093302..2c0476f2e1faf3d 100644 --- a/Lib/test/test_capi/test_bytes.py +++ b/Lib/test/test_capi/test_bytes.py @@ -389,6 +389,24 @@ def test_resize(self): writer.resize(len(b'number=123456'), b'456') self.assertEqual(writer.finish(), self.result_type(b'number=123456')) + def test_resize_error(self): + small_buffer = _testcapi.PyBytesWriter_small_buffer + init = b'x' * (small_buffer * 2) + writer = self.create_writer(len(init), init) + size = len(init) + 100 + try: + with self.assertRaises(MemoryError): + _testcapi.set_nomemory(0) + writer.resize(size, b'') + finally: + _testcapi.remove_mem_hooks() + suffix = b'still working' + writer.write_bytes(suffix, -1) + self.assertEqual(writer.finish(), self.result_type(init + suffix)) + + # Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize) + # if the new size is smaller than the allocated size + def test_format_i(self): # Test PyBytesWriter_Format() writer = self.create_writer() @@ -446,24 +464,6 @@ def test_example_resize(self): def test_example_highlevel(self): self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!') - def test_resize_error(self): - small_buffer = _testcapi.PyBytesWriter_small_buffer - init = b'x' * (small_buffer * 2) - writer = self.create_writer(len(init), init) - size = len(init) + 100 - try: - with self.assertRaises(MemoryError): - _testcapi.set_nomemory(0) - writer.resize(size, b'') - finally: - _testcapi.remove_mem_hooks() - suffix = b'still working' - writer.write_bytes(suffix, -1) - self.assertEqual(writer.finish(), self.result_type(init + suffix)) - - # Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize) - # if the new size is smaller than the allocated size - class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase): result_type = bytearray diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst new file mode 100644 index 000000000000000..fa8de3bd0abb61a --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-02-50-14.gh-issue-157242.LsqOUJ.rst @@ -0,0 +1,3 @@ +If :meth:`bytearray.resize` or :meth:`bytearray.take_bytes` fails, leave the +:class:`bytearray` unchanged, instead of clearing it. Patch by Victor +Stinner. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 5e6639f3dd74c6e..05e1b27dc82558a 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -43,12 +43,24 @@ _getbytevalue(PyObject* arg, int *value) return 1; } +static inline void +bytearray_write_trailing_null_byte(PyByteArrayObject *self) +{ + char *data = PyByteArray_AS_STRING(self); + Py_ssize_t size = PyByteArray_GET_SIZE(self); + data[size] = '\0'; +} + + static void -bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size, - Py_ssize_t alloc) +bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size) { + Py_ssize_t alloc = PyBytes_GET_SIZE(self->ob_bytes_object); + assert(0 <= size && size <= alloc); + /* Only the empty bytes may be immortal. */ assert((alloc == 0) == _Py_IsImmortal(self->ob_bytes_object)); + self->ob_bytes = self->ob_start = PyBytes_AS_STRING(self->ob_bytes_object); Py_SET_SIZE(self, size); FT_ATOMIC_STORE_SSIZE_RELAXED(self->ob_alloc, alloc); @@ -185,7 +197,7 @@ PyByteArray_FromStringAndSize(const char *bytes, Py_ssize_t size) Py_DECREF(new); return NULL; } - bytearray_reinit_from_bytes(new, size, size); + bytearray_reinit_from_bytes(new, size); if (bytes != NULL && size > 0) { memcpy(new->ob_bytes, bytes, size); } @@ -211,6 +223,43 @@ PyByteArray_AsString(PyObject *self) return PyByteArray_AS_STRING(self); } + +static int +bytearray_resize_storage(PyByteArrayObject *self, + Py_ssize_t new_size, Py_ssize_t alloc) +{ + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); + assert(1 <= new_size && new_size <= alloc); + + Py_ssize_t size = Py_SIZE(self); + + /* Re-align data to the start of the allocation. */ + char *old_start = self->ob_start; + if (self->ob_start != self->ob_bytes) { + /* optimization tradeoff: This is faster than a new allocation when + the number of bytes being removed in a resize is small; for + large size changes it may be better to just make a new bytes + object as _PyBytes_Resize will do a malloc + memcpy internally. + */ + Py_ssize_t move = Py_MIN(new_size, size); + memmove(self->ob_bytes, self->ob_start, move); + self->ob_start = self->ob_bytes; + } + + if (_PyBytes_ResizeKeepOnError(&self->ob_bytes_object, alloc) < 0) { + if (old_start != self->ob_bytes && new_size < size) { + // Move remaining bytes + Py_ssize_t moved = new_size; + Py_ssize_t remaining = size - moved; + memmove(self->ob_bytes + moved, old_start + moved, remaining); + } + bytearray_write_trailing_null_byte(self); + return -1; + } + return 0; +} + + static int bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) { @@ -246,7 +295,7 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) if (requested_size == 0) { Py_SETREF(obj->ob_bytes_object, Py_GetConstant(Py_CONSTANT_EMPTY_BYTES)); - bytearray_reinit_from_bytes(obj, 0, 0); + bytearray_reinit_from_bytes(obj, 0); return 0; } @@ -261,7 +310,7 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) /* Minor downsize; quick exit */ Py_SET_SIZE(self, size); /* Add mid-buffer null; end provided by bytes. */ - PyByteArray_AS_STRING(self)[size] = '\0'; /* Trailing null */ + bytearray_write_trailing_null_byte(_PyByteArray_CAST(self)); return 0; } } @@ -281,28 +330,16 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) return -1; } - /* Re-align data to the start of the allocation. */ - if (logical_offset > 0) { - /* optimization tradeoff: This is faster than a new allocation when - the number of bytes being removed in a resize is small; for large - size changes it may be better to just make a new bytes object as - _PyBytes_Resize will do a malloc + memcpy internally. */ - memmove(obj->ob_bytes, obj->ob_start, - Py_MIN(requested_size, Py_SIZE(self))); + if (bytearray_resize_storage(obj, requested_size, (Py_ssize_t)alloc) < 0) { + return -1; } - int ret = _PyBytes_Resize(&obj->ob_bytes_object, alloc); - if (ret == -1) { - obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - size = alloc = 0; - } - bytearray_reinit_from_bytes(obj, size, alloc); + bytearray_reinit_from_bytes(obj, size); if (alloc != size) { /* Add mid-buffer null; end provided by bytes. */ - obj->ob_bytes[size] = '\0'; + bytearray_write_trailing_null_byte(obj); } - - return ret; + return 0; } int @@ -928,7 +965,7 @@ bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) } PyByteArrayObject *self = _PyByteArray_CAST(op); self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - bytearray_reinit_from_bytes(self, 0, 0); + bytearray_reinit_from_bytes(self, 0); self->ob_exports = 0; return op; } @@ -994,9 +1031,9 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, if (_PyObject_IsUniquelyReferenced(encoded) && PyBytes_CheckExact(encoded)) { - Py_ssize_t size = Py_SIZE(encoded); + Py_ssize_t size = PyBytes_GET_SIZE(encoded); self->ob_bytes_object = encoded; - bytearray_reinit_from_bytes(self, size, size); + bytearray_reinit_from_bytes(self, size); return 0; } new = bytearray_iconcat((PyObject*)self, encoded); @@ -1120,7 +1157,7 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, /* Append the byte */ if (Py_SIZE(self) + 1 < self->ob_alloc) { Py_SET_SIZE(self, Py_SIZE(self) + 1); - PyByteArray_AS_STRING(self)[Py_SIZE(self)] = '\0'; + bytearray_write_trailing_null_byte(self); } else if (PyByteArray_Resize((PyObject *)self, Py_SIZE(self)+1) < 0) goto error; @@ -1610,6 +1647,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n) } Py_ssize_t remaining_length = size - to_take; + // optimization: If taking less than leaving, just copy the small to_take // portion out and move ob_start. if (to_take < remaining_length) { @@ -1631,16 +1669,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n) memcpy(PyBytes_AS_STRING(remaining), self->ob_start + to_take, remaining_length); - // If the bytes are offset inside the buffer must first align. - if (self->ob_start != self->ob_bytes) { - memmove(self->ob_bytes, self->ob_start, to_take); - self->ob_start = self->ob_bytes; - } - - if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) { - assert(self->ob_bytes_object == NULL); - self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - bytearray_reinit_from_bytes(self, 0, 0); + if (bytearray_resize_storage(self, to_take, to_take) < 0) { Py_DECREF(remaining); return NULL; } @@ -1648,7 +1677,7 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n) // Point the bytearray towards the buffer with the remaining data. PyObject *result = self->ob_bytes_object; self->ob_bytes_object = remaining; - bytearray_reinit_from_bytes(self, remaining_length, remaining_length); + bytearray_reinit_from_bytes(self, remaining_length); return result; } From 051b168e63af80872222a2d91d43af4de16980b1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 05:27:05 +0200 Subject: [PATCH 2/4] gh-157242: Fix set_nomemory() on Py_TRACE_REFS build (#157351) On Py_TRACE_REFS build, use malloc() and free() functions of the C library for the "reference chain" hash table. So it becomes possible to use _testcapi.set_nomemory() with Py_TRACE_REFS. Mark new tests using set_nomemory() with @support.nomemtest. --- Lib/test/support/__init__.py | 13 ++++--------- Lib/test/test_bytes.py | 2 ++ Lib/test/test_capi/test_bytes.py | 2 ++ Objects/object.c | 9 +++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 28a0ba6c666629b..210982fae236d5f 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1364,21 +1364,16 @@ def wrapper(self): return wrapper return decorator -def nomemtest(f): +def nomemtest(test): """Check that we can use this test with `_testcapi.set_nomemory`.""" from .import_helper import import_module - @functools.wraps(f) + @functools.wraps(test) def internal(*args, **kwargs): import_module('_testcapi') - return f(*args, **kwargs) + return test(*args, **kwargs) - return unittest.skipIf( - # Python built with Py_TRACE_REFS fail with a fatal error in - # _PyRefchain_Trace() on memory allocation error. - Py_TRACE_REFS, - 'cannot test Py_TRACE_REFS build', - )(cpython_only(internal)) + return cpython_only(internal) def bigaddrspacetest(f): """Decorator for tests that fill the address space.""" diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 0f21ffb5ecb9b4f..4b2bed9a0a56446 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1569,6 +1569,7 @@ def test_resize(self): self.assertRaises(MemoryError, bytearray().resize, sys.maxsize) self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize) + @support.nomemtest def test_resize_error(self): # gh-157242: If bytearray.resize() fails (MemoryError), # the bytearray must be left unchanged. @@ -1662,6 +1663,7 @@ def test_take_bytes(self): self.assertEqual(ba, bytearray(b'A')) self.assertEqual(ord(b'c'), ord('c')) + @support.nomemtest def test_take_bytes_error(self): # gh-157242: If bytearray.take_bytes() fails (MemoryError), # the bytearray must be left unchanged. diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py index 2c0476f2e1faf3d..799a17617be1a3f 100644 --- a/Lib/test/test_capi/test_bytes.py +++ b/Lib/test/test_capi/test_bytes.py @@ -1,5 +1,6 @@ import sys import unittest +from test import support from test.support import import_helper _testlimitedcapi = import_helper.import_module('_testlimitedcapi') @@ -389,6 +390,7 @@ def test_resize(self): writer.resize(len(b'number=123456'), b'456') self.assertEqual(writer.finish(), self.result_type(b'number=123456')) + @support.nomemtest def test_resize_error(self): small_buffer = _testcapi.PyBytesWriter_small_buffer init = b'x' * (small_buffer * 2) diff --git a/Objects/object.c b/Objects/object.c index 856d9fc41a41546..a83f8d4c04ca079 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -198,10 +198,11 @@ refchain_init(PyInterpreterState *interp) return 0; } _Py_hashtable_allocator_t alloc = { - // Don't use default PyMem_Malloc() and PyMem_Free() which - // require the caller to hold the GIL. - .malloc = PyMem_RawMalloc, - .free = PyMem_RawFree, + // Use directly malloc() and free() of the C library. Using + // PyMem_RawMalloc() and PyMem_RawFree() prevents testing + // _testcapi.set_nomemory(). + .malloc = malloc, + .free = free, }; REFCHAIN(interp) = _Py_hashtable_new_full( _Py_hashtable_hash_ptr, _Py_hashtable_compare_direct, From 47029802a6feb532461dc9a0907d6123fe04429b Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sat, 12 Sep 2026 05:39:44 +0200 Subject: [PATCH 3/4] gh-128213: fast path for bytes creation from list and tuple (#132590) Co-authored-by: Ben Hsing Co-authored-by: Kumar Aditya --- .../test_free_threading/test_bytes_object.py | 37 +++++++ ...-12-24-08-44-49.gh-issue-128213.Y71jDi.rst | 3 + Objects/bytesobject.c | 96 ++++++------------- 3 files changed, 69 insertions(+), 67 deletions(-) create mode 100644 Lib/test/test_free_threading/test_bytes_object.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-12-24-08-44-49.gh-issue-128213.Y71jDi.rst diff --git a/Lib/test/test_free_threading/test_bytes_object.py b/Lib/test/test_free_threading/test_bytes_object.py new file mode 100644 index 000000000000000..a371e3d533a2cb9 --- /dev/null +++ b/Lib/test/test_free_threading/test_bytes_object.py @@ -0,0 +1,37 @@ +import unittest +from threading import Thread, Barrier +from test.support import threading_helper + +threading_helper.requires_working_threading(module=True) + + +class BytesThreading(unittest.TestCase): + @threading_helper.reap_threads + def test_conversion_from_mutating_list(self): + number_of_threads = 10 + number_of_iterations = 10 + barrier = Barrier(number_of_threads) + + x = [1, 2, 3, 4, 5] + extends = [(ii,) * (2 + ii) for ii in range(number_of_threads)] + + def work(ii): + barrier.wait() + for _ in range(100): + bytes(x) + x.extend(extends[ii]) + if len(x) > 10: + x[:] = [0] + + for it in range(number_of_iterations): + worker_threads = [] + for ii in range(number_of_threads): + worker_threads.append(Thread(target=work, args=[ii])) + with threading_helper.start_threads(worker_threads): + pass + + barrier.reset() + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-12-24-08-44-49.gh-issue-128213.Y71jDi.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-24-08-44-49.gh-issue-128213.Y71jDi.rst new file mode 100644 index 000000000000000..85e3a7b2840fbc4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-12-24-08-44-49.gh-issue-128213.Y71jDi.rst @@ -0,0 +1,3 @@ +Speed up :class:`bytes` creation from :class:`list` and :class:`tuple` of integers. + +Patch by Ben Hsing and Pieter Eendebak diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index 27ffc6e869ede3c..b84fdcd0ecc0153 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -6,6 +6,7 @@ #include "pycore_bytesobject.h" // _PyBytes_Find(), _PyBytes_RepeatBuffer() #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_ceval.h" // _PyEval_GetBuiltin() +#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST() #include "pycore_format.h" // F_LJUST #include "pycore_freelist.h" // _Py_FREELIST_FREE() #include "pycore_global_objects.h"// _Py_GET_GLOBAL_OBJECT() @@ -2985,82 +2986,39 @@ _PyBytes_FromBuffer(PyObject *x) return NULL; } -static PyObject* -_PyBytes_FromList(PyObject *x) +/* Fast path for a list or tuple of ints. + Return 1 on success (*result set to the new bytes object), + 0 to fall back to the slow path, or -1 on error (with an exception set). */ +static int +_PyBytes_FromSequence_lock_held(PyObject *x, PyObject **result) { - Py_ssize_t size = PyList_GET_SIZE(x); + *result = NULL; + Py_ssize_t size = PySequence_Fast_GET_SIZE(x); PyBytesWriter *writer = PyBytesWriter_Create(size); if (writer == NULL) { - return NULL; + return -1; } - size = _PyBytesWriter_ResizeToAllocated(writer); char *str = PyBytesWriter_GetData(writer); - for (Py_ssize_t i = 0; i < PyList_GET_SIZE(x); i++) { - PyObject *item = _PyList_GetItemRef((PyListObject *)x, i); - if (item == NULL) { - goto error; + PyObject *const *items = PySequence_Fast_ITEMS(x); + for (Py_ssize_t i = 0; i < size; i++) { + Py_ssize_t value = PyLong_AsSsize_t(items[i]); + if (value == -1 && PyErr_Occurred()) { + PyBytesWriter_Discard(writer); + PyErr_Clear(); + return 0; } - Py_ssize_t value = PyNumber_AsSsize_t(item, NULL); - Py_DECREF(item); - if (value == -1 && PyErr_Occurred()) - goto error; if (value < 0 || value >= 256) { PyErr_SetString(PyExc_ValueError, "bytes must be in range(0, 256)"); - goto error; - } - - if (i >= size) { - str = _PyBytesWriter_ResizeAndUpdatePointer(writer, size + 1, str); - if (str == NULL) { - goto error; - } - - // Set the writer size to its allocated size - size = _PyBytesWriter_ResizeToAllocated(writer); - } - *str++ = (char) value; - } - return PyBytesWriter_FinishWithPointer(writer, str); - -error: - PyBytesWriter_Discard(writer); - return NULL; -} - -static PyObject* -_PyBytes_FromTuple(PyObject *x) -{ - Py_ssize_t i, size = PyTuple_GET_SIZE(x); - Py_ssize_t value; - PyObject *item; - - PyBytesWriter *writer = PyBytesWriter_Create(size); - if (writer == NULL) { - return NULL; - } - char *str = PyBytesWriter_GetData(writer); - - for (i = 0; i < size; i++) { - item = PyTuple_GET_ITEM(x, i); - value = PyNumber_AsSsize_t(item, NULL); - if (value == -1 && PyErr_Occurred()) - goto error; - - if (value < 0 || value >= 256) { - PyErr_SetString(PyExc_ValueError, - "bytes must be in range(0, 256)"); - goto error; + PyBytesWriter_Discard(writer); + return -1; } *str++ = (char) value; } - return PyBytesWriter_Finish(writer); - - error: - PyBytesWriter_Discard(writer); - return NULL; + *result = PyBytesWriter_Finish(writer); + return *result != NULL ? 1 : -1; } static PyObject * @@ -3143,11 +3101,15 @@ PyBytes_FromObject(PyObject *x) if (PyObject_CheckBuffer(x)) return _PyBytes_FromBuffer(x); - if (PyList_CheckExact(x)) - return _PyBytes_FromList(x); - - if (PyTuple_CheckExact(x)) - return _PyBytes_FromTuple(x); + if (PyList_CheckExact(x) || PyTuple_CheckExact(x)) { + int rc; + Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(x); + rc = _PyBytes_FromSequence_lock_held(x, &result); + Py_END_CRITICAL_SECTION_SEQUENCE_FAST(); + if (rc != 0) { + return result; + } + } if (!PyUnicode_Check(x)) { it = PyObject_GetIter(x); From 7b17157d8d81366a03c1185b19566b3ff6feec7a Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 05:43:16 +0200 Subject: [PATCH 4/4] gh-155742: Use PyBytesWriter in codeobject.c (#157345) Replace soft deprecated _PyBytes_Resize() with PyBytesWriter. --- Objects/codeobject.c | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 58811d63c7e318c..6b4bae40cc77de1 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -647,19 +647,19 @@ remove_column_info(PyObject *locations) { Py_ssize_t offset = 0; const uint8_t *data = (const uint8_t *)PyBytes_AS_STRING(locations); - PyObject *res = PyBytes_FromStringAndSize(NULL, 32); + PyBytesWriter *res = PyBytesWriter_Create(32); if (res == NULL) { - PyErr_NoMemory(); return NULL; } - uint8_t *output = (uint8_t *)PyBytes_AS_STRING(res); + uint8_t *output = (uint8_t *)PyBytesWriter_GetData(res); while (offset < PyBytes_GET_SIZE(locations)) { - Py_ssize_t write_offset = output - (uint8_t *)PyBytes_AS_STRING(res); - if (write_offset + 16 >= PyBytes_GET_SIZE(res)) { - if (_PyBytes_Resize(&res, PyBytes_GET_SIZE(res) * 2) < 0) { + Py_ssize_t write_offset = output - (uint8_t *)PyBytesWriter_GetData(res); + if (write_offset + 16 >= PyBytesWriter_GetSize(res)) { + if (PyBytesWriter_Resize(res, PyBytesWriter_GetSize(res) * 2) < 0) { + PyBytesWriter_Discard(res); return NULL; } - output = (uint8_t *)PyBytes_AS_STRING(res) + write_offset; + output = (uint8_t *)PyBytesWriter_GetData(res) + write_offset; } int code = (data[offset] >> 3) & 15; if (code == PY_CODE_LOCATION_INFO_NONE) { @@ -678,11 +678,7 @@ remove_column_info(PyObject *locations) offset++; } } - Py_ssize_t write_offset = output - (uint8_t *)PyBytes_AS_STRING(res); - if (_PyBytes_Resize(&res, write_offset)) { - return NULL; - } - return res; + return PyBytesWriter_FinishWithPointer(res, output); } static int