https://github.com/python/cpython/commit/4e530b82f37ec89e3b54bc5d0a1612c643bff1e0
commit: 4e530b82f37ec89e3b54bc5d0a1612c643bff1e0
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-09T20:59:57+02:00
summary:

gh-129813: Check size in PyBytesWriter_FinishWithSize() (#157226)

Reject size larger than the allocated size.

Check also negative size in PyBytesWriter_FinishWithSize() to always
raise ValueError. Previously, the function raised SystemError or
ValueError depending on the code path.

Replace _PyBytesWriter_GetAllocated() optimization with
_PyBytesWriter_ResizeToAllocated() to update the writer size to its
allocated size.

test_capi: only run the 3 "example" tests in BytesWriterTest. There
is no need to rerun the same tests in ByteArrayWriterTest.

files:
M Lib/test/test_capi/test_bytes.py
M Modules/_testcapi/bytes.c
M Objects/bytesobject.c

diff --git a/Lib/test/test_capi/test_bytes.py b/Lib/test/test_capi/test_bytes.py
index 38cda931e7d54f3..4c1431bacef0a27 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -317,14 +317,20 @@ def test_create(self):
 
     def test_finish_with_size(self):
         # Test PyBytesWriter_FinishWithSize()
-        writer = self.create_writer(10, b'abc')
+        writer = self.create_writer(10, b'abcdef')
         self.assertEqual(writer.get_size(), 10)
         self.assertEqual(writer.finish_with_size(3), self.result_type(b'abc'))
 
+        # Error if the size is negative
         writer = self.create_writer(3, b'abc')
-        with self.assertRaises(SystemError):
+        with self.assertRaises(ValueError):
             writer.finish_with_size(-3)
 
+        # Error if the requested size is larger than the allocated size
+        writer = self.create_writer(3, b'abc')
+        with self.assertRaises(ValueError):
+            writer.finish_with_size(4)
+
     def test_write_bytes(self):
          # Test PyBytesWriter_WriteBytes()
          writer = self.create_writer()
@@ -378,15 +384,6 @@ def test_format_i(self):
         writer.format_i(b'y=%i', 456)
         self.assertEqual(writer.finish(), self.result_type(b'x=123, y=456'))
 
-    def test_example_abc(self):
-        self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
-
-    def test_example_resize(self):
-        self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
-
-    def test_example_highlevel(self):
-        self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
-
 
 class BytesWriterTest(BaseWriterTest, unittest.TestCase):
     result_type = bytes
@@ -424,6 +421,15 @@ def test_singletons(self):
             writer.write_bytes(unused_text, len(unused_text))
             self.assertIs(writer.finish_with_size(1), singletons[ch])
 
+    def test_example_abc(self):
+        self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
+
+    def test_example_resize(self):
+        self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
+
+    def test_example_highlevel(self):
+        self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
+
 
 class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
     result_type = bytearray
diff --git a/Modules/_testcapi/bytes.c b/Modules/_testcapi/bytes.c
index a868c684cc987cd..4830cc8b54bd837 100644
--- a/Modules/_testcapi/bytes.c
+++ b/Modules/_testcapi/bytes.c
@@ -353,12 +353,114 @@ byteswriter_highlevel(PyObject *Py_UNUSED(module), 
PyObject *Py_UNUSED(args))
 }
 
 
+static size_t
+pybyteswriter_small_buffer_size(void)
+{
+    return offsetof(PyBytesWriter, obj);
+}
+
+
+// Test the "Pointer" API of PyBytesWriter
+static PyObject *
+test_byteswriter_ptr(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
+{
+    // Test PyBytesWriter_FinishWithPointer(): create the string "abc"
+    PyBytesWriter *writer = PyBytesWriter_Create(3);
+    if (writer == NULL) {
+        return NULL;
+    }
+    char *str = PyBytesWriter_GetData(writer);
+    memcpy(str, "abc", 3);
+    str += 3;
+    PyObject *result = PyBytesWriter_FinishWithPointer(writer, str);
+    if (result == NULL) {
+        return NULL;
+    }
+    assert(PyBytes_GET_SIZE(result) == 3);
+    assert(memcmp(PyBytes_AS_STRING(result), "abc", 3) == 0);
+    Py_DECREF(result);
+
+    // Test PyBytesWriter_GrowAndUpdatePointer().
+    // Start by using the small buffer, and then resize to use a bytes object.
+    writer = PyBytesWriter_Create(0);
+    if (writer == NULL) {
+        return NULL;
+    }
+    str = PyBytesWriter_GetData(writer);
+
+    str = PyBytesWriter_GrowAndUpdatePointer(writer, 100, str);
+    if (str == NULL) {
+        PyBytesWriter_Discard(writer);
+        return NULL;
+    }
+    memset(str, 'x', 100);
+    str += 100;
+
+    // make sure that the test switchs to a bytes object
+    assert((100 + 200) > pybyteswriter_small_buffer_size());
+    char *old_str = str;
+    str = PyBytesWriter_GrowAndUpdatePointer(writer, 200, str);
+    if (str == NULL) {
+        PyBytesWriter_Discard(writer);
+        return NULL;
+    }
+    // make sure that we moved from the small buffer to a bytes object
+    assert(str != old_str);
+    memset(str, 'y', 200);
+    str += 200;
+
+    result = PyBytesWriter_FinishWithPointer(writer, str);
+    if (result == NULL) {
+        return NULL;
+    }
+    assert(PyBytes_GET_SIZE(result) == 300);
+    str = PyBytes_AS_STRING(result);
+    for (Py_ssize_t i=0; i < 100; i++) {
+        assert(str[i] == 'x');
+    }
+    for (Py_ssize_t i=0; i < 200; i++) {
+        assert(str[100 + i] == 'y');
+    }
+    Py_DECREF(result);
+
+    // Check that PyBytesWriter_FinishWithPointer() rejects pointer
+    // after the buffer end (create a string larger than the allocated size)
+    writer = PyBytesWriter_Create(3);
+    if (writer == NULL) {
+        return NULL;
+    }
+    str = PyBytesWriter_GetData(writer);
+    memcpy(str, "abc", 3);
+    str += 4;  // off-by-one bug on purpose
+    result = PyBytesWriter_FinishWithPointer(writer, str);
+    assert(result == NULL);
+    assert(PyErr_ExceptionMatches(PyExc_ValueError));
+    PyErr_Clear();
+
+    // Check that PyBytesWriter_FinishWithPointer() rejects pointer
+    // before the buffer start (negative size)
+    writer = PyBytesWriter_Create(3);
+    if (writer == NULL) {
+        return NULL;
+    }
+    str = PyBytesWriter_GetData(writer);
+    str--;     // bug on purpose: go before the buffer start
+    result = PyBytesWriter_FinishWithPointer(writer, str);
+    assert(result == NULL);
+    assert(PyErr_ExceptionMatches(PyExc_ValueError));
+    PyErr_Clear();
+
+    Py_RETURN_NONE;
+}
+
+
 static PyMethodDef test_methods[] = {
     {"bytes_resize", bytes_resize, METH_VARARGS},
     {"bytes_join", bytes_join, METH_VARARGS},
     {"byteswriter_abc", byteswriter_abc, METH_NOARGS},
     {"byteswriter_resize", byteswriter_resize, METH_NOARGS},
     {"byteswriter_highlevel", byteswriter_highlevel, METH_NOARGS},
+    {"test_byteswriter_ptr", test_byteswriter_ptr, METH_NOARGS},
     {NULL},
 };
 
@@ -380,7 +482,7 @@ _PyTestCapi_Init_Bytes(PyObject *m)
     Py_DECREF(writer_type);
 
     // PyBytesWriter.obj is the second member, small_buffer is the first member
-    long size = (long)offsetof(PyBytesWriter, obj);
+    long size = (long)pybyteswriter_small_buffer_size();
     if (PyModule_AddIntConstant(m, "PyBytesWriter_small_buffer", size) < 0) {
         Py_DECREF(writer_type);
         return -1;
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 1b8dca734974153..2ae55b33f4f49d7 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -31,7 +31,7 @@ class bytes "PyBytesObject *" "&PyBytes_Type"
 /* Forward declaration */
 static void* _PyBytesWriter_ResizeAndUpdatePointer(PyBytesWriter *writer,
                                                    Py_ssize_t size, void 
*data);
-static Py_ssize_t _PyBytesWriter_GetAllocated(PyBytesWriter *writer);
+static Py_ssize_t _PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer);
 
 
 #define CHARACTERS _Py_SINGLETON(bytes_characters)
@@ -2993,8 +2993,8 @@ _PyBytes_FromList(PyObject *x)
     if (writer == NULL) {
         return NULL;
     }
+    size = _PyBytesWriter_ResizeToAllocated(writer);
     char *str = PyBytesWriter_GetData(writer);
-    size = _PyBytesWriter_GetAllocated(writer);
 
     for (Py_ssize_t i = 0; i < PyList_GET_SIZE(x); i++) {
         PyObject *item = _PyList_GetItemRef((PyListObject *)x, i);
@@ -3017,7 +3017,9 @@ _PyBytes_FromList(PyObject *x)
             if (str == NULL) {
                 goto error;
             }
-            size = _PyBytesWriter_GetAllocated(writer);
+
+            // Set the writer size to its allocated size
+            size = _PyBytesWriter_ResizeToAllocated(writer);
         }
         *str++ = (char) value;
     }
@@ -3075,8 +3077,8 @@ _PyBytes_FromIterator(PyObject *it, PyObject *x)
     if (writer == NULL) {
         return NULL;
     }
+    size = _PyBytesWriter_ResizeToAllocated(writer);
     char *str = PyBytesWriter_GetData(writer);
-    size = _PyBytesWriter_GetAllocated(writer);
 
     /* Run the iterator to exhaustion */
     for (i = 0; ; i++) {
@@ -3110,7 +3112,9 @@ _PyBytes_FromIterator(PyObject *it, PyObject *x)
             if (str == NULL) {
                 goto error;
             }
-            size = _PyBytesWriter_GetAllocated(writer);
+
+            // Set the writer size to its allocated size
+            size = _PyBytesWriter_ResizeToAllocated(writer);
         }
         *str++ = (char) value;
     }
@@ -3747,6 +3751,19 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
 PyObject*
 PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
 {
+    // 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.
+    if (size < 0) {
+        PyErr_Format(PyExc_ValueError, "size must be positive");
+        goto error;
+    }
+
+    if (size > writer->size) {
+        PyErr_SetString(PyExc_ValueError, "size larger than allocated size");
+        goto error;
+    }
+
     PyObject *result;
     if (size == 0) {
         result = bytes_get_empty();
@@ -3804,12 +3821,6 @@ PyObject*
 PyBytesWriter_FinishWithPointer(PyBytesWriter *writer, void *buf)
 {
     Py_ssize_t size = (char*)buf - byteswriter_data(writer);
-    if (size < 0 || size > byteswriter_allocated(writer)) {
-        PyBytesWriter_Discard(writer);
-        PyErr_SetString(PyExc_ValueError, "invalid end pointer");
-        return NULL;
-    }
-
     return PyBytesWriter_FinishWithSize(writer, size);
 }
 
@@ -3828,13 +3839,6 @@ PyBytesWriter_GetSize(PyBytesWriter *writer)
 }
 
 
-static Py_ssize_t
-_PyBytesWriter_GetAllocated(PyBytesWriter *writer)
-{
-    return byteswriter_allocated(writer);
-}
-
-
 int
 PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
 {
@@ -3934,3 +3938,15 @@ PyBytesWriter_Format(PyBytesWriter *writer, const char 
*format, ...)
     Py_ssize_t size = buf - byteswriter_data(writer);
     return PyBytesWriter_Resize(writer, size);
 }
+
+
+// Resize the writer to its allocated size.
+// Return the new size.
+// The function cannot fail.
+static Py_ssize_t
+_PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer)
+{
+    Py_ssize_t allocated = byteswriter_allocated(writer);
+    writer->size = allocated;
+    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