https://github.com/python/cpython/commit/e2ff498cc03886b73fca0429ee745b04646ad8e0
commit: e2ff498cc03886b73fca0429ee745b04646ad8e0
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-18T02:43:38Z
summary:

gh-157242: Add byteswriter_check_consistency() (#157723)

Add byteswriter_check_consistency() to check that the bytes/bytearray
object can be mutated. It is called by all PyBytesWriter functions.

* Add test_thread to PyBytesWriter tests.
* _PyBytes_IsMutable() now checks _PyObject_IsUniquelyReferenced().
* Clarify thread safety in PyBytesWriter documentation.

files:
M Doc/c-api/bytes.rst
M Include/internal/pycore_unicodeobject.h
M Lib/test/test_capi/test_bytes.py
M Objects/bytesobject.c

diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst
index c03816b5727fa4..fbdad0f346550d 100644
--- a/Doc/c-api/bytes.rst
+++ b/Doc/c-api/bytes.rst
@@ -305,8 +305,8 @@ object.
 
    A bytes writer object.
 
-   The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only
-   be used by a single thread, it must not be shared between threads.
+   The API is **not thread safe**. To share a writer with multiple threads, a
+   critical section or a lock is needed.
 
    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_unicodeobject.h 
b/Include/internal/pycore_unicodeobject.h
index acd6bcf0813f9f..e9a4aed37030e7 100644
--- a/Include/internal/pycore_unicodeobject.h
+++ b/Include/internal/pycore_unicodeobject.h
@@ -116,7 +116,8 @@ _PyUnicodeWriter_CanWrite(_PyUnicodeWriter *writer)
     PyObject *buffer = writer->buffer;
     assert(buffer != NULL);
     // Do not use _PyObject_IsUniquelyReferenced(): the caller can have its own
-    // lock to prevent a writer being used by two theads at the same time.
+    // lock to prevent a writer from being used by two threads at the same
+    // time.
     assert(Py_REFCNT(buffer) == 1);
     assert(PyUnstable_Unicode_GET_CACHED_HASH(buffer) == -1);
     assert(!PyUnicode_CHECK_INTERNED(buffer));
diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py
index b68412a02b3228..1356e5d6c51c14 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -1,8 +1,10 @@
 import sys
 import textwrap
+import threading
 import unittest
 from test import support
 from test.support import import_helper
+from test.support import threading_helper
 from test.support.script_helper import assert_python_failure
 
 _testlimitedcapi = import_helper.import_module('_testlimitedcapi')
@@ -461,6 +463,14 @@ def test_resize(self):
         self.assertEqual(writer.finish(),
                          b's' * small + b'L' * (large - small))
 
+        # Make sure that it's possible to write after a resize to zero
+        # when a bytes/bytearray object is allocated.
+        writer = self.create_writer()
+        writer.resize(self.LARGE_BUFFER)
+        writer.resize(0)
+        writer.write_bytes(b'abc', 3)
+        self.assertEqual(writer.finish(), b'abc')
+
         # invalid size
         for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
             with self.subTest(size=size):
@@ -665,6 +675,36 @@ def test_get_data_canary(self):
         self.assertEqual(get_data_canary(writer),
                          b'abc123' + CANARY_BYTE)
 
+    @threading_helper.requires_working_threading()
+    def test_thread(self):
+        # PyBytesWriter can be used by multiple threads: it's up to the caller
+        # to implement a lock to prevent concurrent accesses.
+        writer = self.create_writer(0)
+        size = None
+        data = None
+
+        def thread_func(writer, LARGE_BUFFER):
+            nonlocal size, data
+
+            # create a bytes object for the buffer
+            writer.write_bytes(b'x' * LARGE_BUFFER, LARGE_BUFFER)
+
+            # so we can check a write with a bytes object
+            writer.write_bytes(b'yz', 2)
+            writer.format_i(b'i=%i', 5)
+            writer.resize(10)
+            data = writer.get_data()
+            size = writer.get_size()
+
+        thread = threading.Thread(target=thread_func,
+                                  args=(writer, self.LARGE_BUFFER))
+        thread.start()
+        threading_helper.join_thread(thread)
+
+        self.assertEqual(size, 10)
+        self.assertEqual(data, b'x' * 10)
+        self.assertEqual(writer.finish(), b'x' * 10)
+
 
 class BytesWriterTest(BaseWriterTest, unittest.TestCase):
     RESULT_TYPE = bytes
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 38ec7a4aefcf56..7fefd64eefb2ac 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -3329,18 +3329,22 @@ PyBytes_ConcatAndDel(PyObject **pv, PyObject *w)
 //
 // Usage: assert(_PyBytes_IsMutable(obj)).
 int
-_PyBytes_IsMutable(PyObject *v)
+_PyBytes_IsMutable(PyObject *self)
 {
-    // Singleton objects must never be modified
-    assert(!_Py_IsImmortal(v));
+    assert(PyBytes_Check(self));
+    // Do not use _PyObject_IsUniquelyReferenced(): this function is called
+    // by bytearray and PyBytesWriter which can be used by multiple threads.
+    assert(Py_REFCNT(self) == 1);
+    assert(!_Py_IsImmortal(self));
 
-    Py_ssize_t size = PyBytes_GET_SIZE(v);
+    // Check that the object is not a singleton
+    Py_ssize_t size = PyBytes_GET_SIZE(self);
     if (size == 0) {
-        assert(v != bytes_get_empty());
+        assert(self != bytes_get_empty());
     }
     else if (size == 1) {
-        unsigned char ch = PyBytes_AS_STRING(v)[0];
-        assert(v != (PyObject*)CHARACTER(ch));
+        unsigned char ch = PyBytes_AS_STRING(self)[0];
+        assert(self != (PyObject*)CHARACTER(ch));
     }
     return 1;
 }
@@ -3675,20 +3679,6 @@ byteswriter_allocated(PyBytesWriter *writer)
 
 
 #ifdef Py_DEBUG
-static void
-byteswriter_check_canary_byte(PyBytesWriter *writer)
-{
-    const unsigned char *data = (const unsigned char*)byteswriter_data(writer);
-    unsigned char canary = data[writer->size];
-    if (canary != PyBytesWriter_CANARY_BYTE) {
-        _Py_FatalErrorFormat(__func__,
-                             "Buffer overflow detected in PyBytesWriter %p "
-                             "at position %zd",
-                             writer, writer->size);
-    }
-}
-
-
 static void
 byteswriter_write_canary_byte(PyBytesWriter *writer)
 {
@@ -3710,6 +3700,45 @@ byteswriter_reset_trailing_byte(PyBytesWriter *writer)
 #endif
 
 
+#ifndef NDEBUG
+static int
+byteswriter_check_consistency(PyBytesWriter *writer)
+{
+    PyObject *obj = writer->obj;
+    if (obj != NULL) {
+        if (writer->use_bytearray) {
+            assert(PyByteArray_CheckExact(obj));
+            // Do not use _PyObject_IsUniquelyReferenced(): the caller can have
+            // its own lock to prevent a writer from being used by two threads
+            // at the same time.
+            assert(Py_REFCNT(obj) == 1);
+            PyByteArrayObject *bytearray = (PyByteArrayObject*)obj;
+            obj = bytearray->ob_bytes_object;
+            assert(obj != NULL);
+        }
+
+        // Code adapted from _PyBytes_IsMutable()
+        assert(PyBytes_CheckExact(obj));
+        assert(_PyBytes_IsMutable(obj));
+        // -1 since the last small buffer byte is used as the canary byte
+        assert((size_t)PyBytes_GET_SIZE(obj) > (sizeof(writer->small_buffer) - 
1));
+    }
+
+#ifdef Py_DEBUG
+    const unsigned char *data = (const unsigned char*)byteswriter_data(writer);
+    unsigned char canary = data[writer->size];
+    if (canary != PyBytesWriter_CANARY_BYTE) {
+        _Py_FatalErrorFormat(__func__,
+                             "Buffer overflow detected in PyBytesWriter %p "
+                             "at position %zd",
+                             writer, writer->size);
+    }
+#endif
+    return 1;
+}
+#endif
+
+
 #ifdef MS_WINDOWS
    /* On Windows, overallocate by 50% is the best factor */
 #  define OVERALLOCATE_FACTOR 2
@@ -3743,6 +3772,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
new_size, int resize)
                 // bytearray can override the canary byte on error
                 byteswriter_write_canary_byte(writer);
 #endif
+                assert(byteswriter_check_consistency(writer));
                 return -1;
             }
         }
@@ -3750,6 +3780,7 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t 
new_size, int resize)
             // Can raise MemoryError or OverflowError
             if (_PyBytes_ResizeKeepOnError(&writer->obj, alloc)) {
                 assert(writer->obj != NULL);
+                assert(byteswriter_check_consistency(writer));
                 return -1;
             }
             assert(_PyBytes_IsMutable(writer->obj));
@@ -3821,7 +3852,7 @@ byteswriter_create(Py_ssize_t size, int use_bytearray)
     if (size >= 1) {
         if (byteswriter_resize(writer, size, 0) < 0) {
 #ifdef Py_DEBUG
-            // Write the canary byte so byteswriter_check_canary_byte()
+            // Write the canary byte so byteswriter_check_consistency()
             // doesn't fail in PyBytesWriter_Discard()
             byteswriter_write_canary_byte(writer);
 #endif
@@ -3835,6 +3866,7 @@ byteswriter_create(Py_ssize_t size, int use_bytearray)
            byteswriter_allocated(writer));
     byteswriter_write_canary_byte(writer);
 #endif
+    assert(byteswriter_check_consistency(writer));
     return writer;
 }
 
@@ -3858,8 +3890,8 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
         return;
     }
 
+    assert(byteswriter_check_consistency(writer));
 #ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
     if (writer->obj != NULL) {
         byteswriter_reset_trailing_byte(writer);
     }
@@ -3873,6 +3905,8 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
 PyObject*
 PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
 {
+    assert(byteswriter_check_consistency(writer));
+
     // Check for negative size here to raise ValueError in all cases, rather
     // than having a different exception depending on the code path. For
     // example, _PyBytes_Resize() raises SystemError on negative size.
@@ -3886,10 +3920,6 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer, 
Py_ssize_t size)
         goto error;
     }
 
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
-
     PyObject *result;
     if (size == 0) {
         result = bytes_get_empty();
@@ -3938,7 +3968,7 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer, 
Py_ssize_t size)
     }
 
 #ifdef Py_DEBUG
-    // Reset the writer, so byteswriter_check_canary_byte() doesn't fail
+    // Reset the writer, so byteswriter_check_consistency() doesn't fail
     // in PyBytesWriter_Discard().
     writer->size = 0;
     byteswriter_write_canary_byte(writer);
@@ -3970,9 +4000,7 @@ PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, 
void *buf)
 void*
 PyBytesWriter_GetData(PyBytesWriter *writer)
 {
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
+    assert(byteswriter_check_consistency(writer));
 
     return byteswriter_data(writer);
 }
@@ -3981,9 +4009,7 @@ PyBytesWriter_GetData(PyBytesWriter *writer)
 Py_ssize_t
 PyBytesWriter_GetSize(PyBytesWriter *writer)
 {
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
+    assert(byteswriter_check_consistency(writer));
 
     return _PyBytesWriter_GetSize(writer);
 }
@@ -3992,9 +4018,7 @@ PyBytesWriter_GetSize(PyBytesWriter *writer)
 int
 PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t new_size)
 {
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
+    assert(byteswriter_check_consistency(writer));
 
     if (new_size < 0) {
         PyErr_SetString(PyExc_ValueError, "size must be >= 0");
@@ -4012,6 +4036,7 @@ PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t 
new_size)
 #ifdef Py_DEBUG
     byteswriter_write_canary_byte(writer);
 #endif
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -4031,9 +4056,7 @@ _PyBytesWriter_ResizeAndUpdatePointer(PyBytesWriter 
*writer, Py_ssize_t size,
 int
 PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow)
 {
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
+    assert(byteswriter_check_consistency(writer));
 
     if (grow == 0) {
         // Nothing to do
@@ -4064,6 +4087,7 @@ PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t grow)
 #ifdef Py_DEBUG
     byteswriter_write_canary_byte(writer);
 #endif
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -4099,6 +4123,8 @@ PyBytesWriter_WriteBytes(PyBytesWriter *writer,
     }
     char *buf = byteswriter_data(writer);
     memcpy(buf + pos, bytes, size);
+
+    assert(byteswriter_check_consistency(writer));
     return 0;
 }
 
@@ -4127,14 +4153,12 @@ PyBytesWriter_Format(PyBytesWriter *writer, const char 
*format, ...)
 static Py_ssize_t
 _PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer)
 {
-#ifdef Py_DEBUG
-    byteswriter_check_canary_byte(writer);
-#endif
-
     Py_ssize_t allocated = byteswriter_allocated(writer);
     writer->size = allocated;
 #ifdef Py_DEBUG
     byteswriter_write_canary_byte(writer);
 #endif
+
+    assert(byteswriter_check_consistency(writer));
     return allocated;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to