https://github.com/python/cpython/commit/c38a347e2c3199d7d3f6a9eefbf46df52caf725d
commit: c38a347e2c3199d7d3f6a9eefbf46df52caf725d
branch: 3.15
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-10-12T21:32:00Z
summary:
[3.15] gh-129813: Check size in PyBytesWriter_FinishWithSize() (#157226)
(#159261)
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.
(cherry picked from commit 4e530b82f37ec89e3b54bc5d0a1612c643bff1e0)
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 eee201b3a75bbd..3d8270ddcf6244 100644
--- a/Lib/test/test_capi/test_bytes.py
+++ b/Lib/test/test_capi/test_bytes.py
@@ -316,11 +316,11 @@ def test_join(self):
bytes_join(b'', NULL)
-class BytesWriterTest(unittest.TestCase):
- RESULT_TYPE = bytes
+class BaseWriterTest:
+ RESULT_TYPE = NotImplementedError
def create_writer(self, alloc=0, string=b''):
- return _testcapi.PyBytesWriter(alloc, string, 0)
+ raise NotImplementedError
def bytes_equal(self, result, expected):
# Similar to assertEqual(), but check also that the result type
@@ -340,14 +340,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.bytes_equal(writer.finish_with_size(3), 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()
@@ -420,6 +426,36 @@ def test_format_s_memory_error(self):
writer.write_bytes(b'after', -1)
self.bytes_equal(writer.finish(), b'after')
+
+class BytesWriterTest(BaseWriterTest, unittest.TestCase):
+ RESULT_TYPE = bytes
+
+ def create_writer(self, alloc=0, string=b''):
+ # Test PyBytesWriter_Create()
+ return _testcapi.PyBytesWriter(alloc, string, 0)
+
+ # Only PyBytesWriter_Create() returns singletons
+ def test_singletons(self):
+ empty = b''
+ singletons = {ch: bytes((ch,)) for ch in range(256)}
+ small_buffer = _testcapi.PyBytesWriter_small_buffer
+
+ writer = self.create_writer()
+ self.assertIs(writer.finish(), empty)
+
+ # Test writer larger than small_buffer
+ writer = self.create_writer()
+ unused_text = b'x' * (small_buffer * 2)
+ writer.write_bytes(unused_text, len(unused_text))
+ self.assertIs(writer.finish_with_size(0), empty)
+
+ for ch in range(256):
+ text = bytes((ch,))
+
+ writer = self.create_writer()
+ writer.write_bytes(text, 1)
+ self.assertIs(writer.finish(), singletons[ch])
+
def test_example_abc(self):
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
@@ -430,10 +466,11 @@ def test_example_highlevel(self):
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
-class ByteArrayWriterTest(BytesWriterTest):
+class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
RESULT_TYPE = bytearray
def create_writer(self, alloc=0, string=b''):
+ # Test private _PyBytesWriter_CreateByteArray()
return _testcapi.PyBytesWriter(alloc, string, 1)
diff --git a/Modules/_testcapi/bytes.c b/Modules/_testcapi/bytes.c
index 23b065aba869cd..735d8d67ca14c8 100644
--- a/Modules/_testcapi/bytes.c
+++ b/Modules/_testcapi/bytes.c
@@ -4,6 +4,8 @@
#include "parts.h"
#include "util.h"
+#include <stddef.h> // offsetof()
+
#include "pycore_bytesobject.h" // _PyBytesWriter_CreateByteArray()
@@ -149,9 +151,9 @@ writer_write_bytes(PyObject *self_raw, PyObject *args)
return NULL;
}
- char *bytes;
- Py_ssize_t size;
- if (!PyArg_ParseTuple(args, "yn", &bytes, &size)) {
+ const char *bytes;
+ Py_ssize_t unused_size, size;
+ if (!PyArg_ParseTuple(args, "y#n", &bytes, &unused_size, &size)) {
return NULL;
}
@@ -373,12 +375,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},
};
@@ -399,5 +503,12 @@ _PyTestCapi_Init_Bytes(PyObject *m)
}
Py_DECREF(writer_type);
+ // PyBytesWriter.obj is the second member, small_buffer is the first member
+ long size = (long)pybyteswriter_small_buffer_size();
+ if (PyModule_AddIntConstant(m, "PyBytesWriter_small_buffer", size) < 0) {
+ Py_DECREF(writer_type);
+ return -1;
+ }
+
return 0;
}
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 0a8b63ea1dd104..7d8d22b32843f0 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -32,7 +32,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)
@@ -2982,8 +2982,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);
@@ -3006,7 +3006,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;
}
@@ -3064,8 +3066,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++) {
@@ -3099,7 +3101,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;
}
@@ -3734,6 +3738,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 && !writer->use_bytearray) {
result = bytes_get_empty();
@@ -3781,12 +3798,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);
}
@@ -3805,13 +3816,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)
{
@@ -3918,3 +3922,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]