From 12c27b6127424f0e0bcffb859e209584e700afb1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 20:07:27 +0200 Subject: [PATCH 1/3] gh-157242: Add _PyBytes_IsMutable() assertion Elaborate mutability in _PyBytes_Resize() and PyBytesWriter documentation. Check _PyBytes_IsMutable() in functions which require a mutable bytes object like PyBytesWriter_Resize(). --- Doc/c-api/bytes.rst | 15 +++++++++--- Include/internal/pycore_bytesobject.h | 4 ++++ Objects/bytearrayobject.c | 1 + Objects/bytesobject.c | 34 +++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst index ff68ecafcda4d0..8448ee04d43c0f 100644 --- a/Doc/c-api/bytes.rst +++ b/Doc/c-api/bytes.rst @@ -231,6 +231,7 @@ called with a non-bytes parameter. Resize a bytes object. *newsize* will be the new length of the bytes object. You can think of it as creating a new bytes object and destroying the old one, only more efficiently. + Pass the address of an existing bytes object as an lvalue (it may be written into), and the new size desired. On success, *\*bytes* holds the resized bytes object and ``0`` is @@ -239,6 +240,14 @@ called with a non-bytes parameter. *\*bytes* is set to ``NULL``, :exc:`MemoryError` is set, and ``-1`` is returned. + While bytes objects are usually immutable in Python, this special C API + allows mutating a bytes object in-place. The returned bytes object can still + be mutated using :c:func:`PyBytesWriter_GetData`; except if *newsize* is + zero in which case it returns the immutable empty bytes string. + + This API is now soft depreacated and the :c:type:`PyBytesWriter` API should + be used instead. + .. soft-deprecated:: 3.15 Use the :c:type:`PyBytesWriter` API instead. @@ -290,10 +299,10 @@ object. .. c:type:: PyBytesWriter - A bytes writer instance. + A bytes writer object. - The API is **not thread safe**: a writer should only be used by a single - thread at the same time. + The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only + be used in a single thread, it must not be shared between threads. The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on success, or :c:func:`PyBytesWriter_Discard` on error. diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h index 32da177c637c26..443bdb26ff8738 100644 --- a/Include/internal/pycore_bytesobject.h +++ b/Include/internal/pycore_bytesobject.h @@ -77,6 +77,10 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n); extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize); +#ifndef NDEBUG +extern int _PyBytes_IsMutable(PyObject *obj); +#endif + /* --- PyBytesWriter ------------------------------------------------------ */ struct PyBytesWriter { diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index 05e1b27dc82558..de30c6118ba176 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -256,6 +256,7 @@ bytearray_resize_storage(PyByteArrayObject *self, bytearray_write_trailing_null_byte(self); return -1; } + assert(_PyBytes_IsMutable(self->ob_bytes_object)); return 0; } diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index ace5fe9d86a7d3..4f98bdd89cc381 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -37,7 +37,7 @@ static Py_ssize_t _PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer); #define CHARACTERS _Py_SINGLETON(bytes_characters) #define CHARACTER(ch) \ - ((PyBytesObject *)&(CHARACTERS[ch])); + ((PyBytesObject *)&(CHARACTERS[ch])) #define EMPTY (&_Py_SINGLETON(bytes_empty)) @@ -3294,6 +3294,29 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w) } +#ifndef NDEBUG +// Make sure that a bytes object can still be mutated. +// +// Usage: assert(_PyBytes_IsMutable(obj)). +int +_PyBytes_IsMutable(PyObject *v) +{ + // Singleton objects must never be modified + assert(!_Py_IsImmortal(v)); + + Py_ssize_t size = PyBytes_GET_SIZE(v); + if (size == 0) { + assert(v != bytes_get_empty()); + } + else if (size == 1) { + unsigned char ch = PyBytes_AS_STRING(v)[0]; + assert(v != (PyObject*)CHARACTER(ch)); + } + return 1; +} +#endif + + /* The following function breaks the notion that bytes are immutable: it changes the size of a bytes object. You can think of it as creating a new bytes object and destroying the old one, only @@ -3331,6 +3354,7 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize) } *pv = result; Py_DECREF(v); + assert(_PyBytes_IsMutable(*pv)); return 0; } @@ -3352,9 +3376,12 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize) Py_MIN(oldsize, newsize)); *pv = result; Py_DECREF(v); + assert(_PyBytes_IsMutable(*pv)); return 0; } - assert(v != bytes_get_empty()); + + // Only mutable bytes can be resized in-place + assert(_PyBytes_IsMutable(v)); if ((size_t)newsize > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) { PyErr_SetString(PyExc_OverflowError, @@ -3385,6 +3412,7 @@ _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize) Py_SET_SIZE(sv, newsize); sv->ob_sval[newsize] = '\0'; set_ob_shash(sv, -1); /* invalidate cached hash value */ + assert(_PyBytes_IsMutable(*pv)); return 0; } @@ -3647,6 +3675,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize) assert(writer->obj != NULL); return -1; } + assert(_PyBytes_IsMutable(writer->obj)); } assert(writer->obj != NULL); } @@ -3673,6 +3702,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize) writer->small_buffer, sizeof(writer->small_buffer)); } + assert(_PyBytes_IsMutable(writer->obj)); } #ifdef Py_DEBUG From f5e5b9a5143184ef985c6fee55bce5cacf84edb7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 21:04:13 +0200 Subject: [PATCH 2/3] Remove redundant paragraph --- Doc/c-api/bytes.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst index 8448ee04d43c0f..8cbc6fe929e931 100644 --- a/Doc/c-api/bytes.rst +++ b/Doc/c-api/bytes.rst @@ -245,9 +245,6 @@ called with a non-bytes parameter. be mutated using :c:func:`PyBytesWriter_GetData`; except if *newsize* is zero in which case it returns the immutable empty bytes string. - This API is now soft depreacated and the :c:type:`PyBytesWriter` API should - be used instead. - .. soft-deprecated:: 3.15 Use the :c:type:`PyBytesWriter` API instead. From 176a3055f1ece0d806da86aa8285da895baa1f3b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 12 Sep 2026 21:47:41 +0200 Subject: [PATCH 3/3] in => by --- Doc/c-api/bytes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst index 8cbc6fe929e931..6618f8ae1a6166 100644 --- a/Doc/c-api/bytes.rst +++ b/Doc/c-api/bytes.rst @@ -299,7 +299,7 @@ object. A bytes writer object. The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only - be used in a single thread, it must not be shared between threads. + be used by a single thread, it must not be shared between threads. The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on success, or :c:func:`PyBytesWriter_Discard` on error.