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

gh-157710: Detect overflow in PyUnicodeWriter_Finish() (#157715)

Check if the trailing null character has been modified to detect
buffer overflow in C extensions.

_PyUnicode_CheckConsistency() now always check if the trailing null
character has been overridden to detect buffer overflow. Previously,
it was only been checked if check_content parameter was non-zero.

files:
A Misc/NEWS.d/next/C_API/2026-09-18-00-29-49.gh-issue-157710.Q_AD8D.rst
M Lib/test/test_capi/test_unicode.py
M Modules/_testinternalcapi.c
M Objects/unicode_writer.c
M Objects/unicodeobject.c

diff --git a/Lib/test/test_capi/test_unicode.py 
b/Lib/test/test_capi/test_unicode.py
index b74bec15edcd93a..f2b77e3fdb5fc4f 100644
--- a/Lib/test/test_capi/test_unicode.py
+++ b/Lib/test/test_capi/test_unicode.py
@@ -1,7 +1,9 @@
-import unittest
 import sys
+import textwrap
+import unittest
 from test import support
 from test.support import threading_helper
+from test.support.script_helper import assert_python_failure
 
 try:
     import _testcapi
@@ -1992,6 +1994,22 @@ def test_singletons(self):
                 writer.write_substring(ch + 'xxx', 0, 1)
                 self.assertIs(writer.finish(), ch)
 
+    @unittest.skipUnless(support.Py_DEBUG, 'need debug build (Py_DEBUG)')
+    def test_detect_overflow(self):
+        # Test detection of buffer overflow
+        code = textwrap.dedent('''
+            from test.support import SuppressCrashReport
+            import _testinternalcapi
+
+            SuppressCrashReport().__enter__()
+            _testinternalcapi.unicodewriter_overflow()
+        ''')
+        proc = assert_python_failure('-c', code)
+        self.assertIn(b'Buffer overflow detected in PyUnicodeWriter', proc.err)
+        # Do not test the position value since it depends on the overallocation
+        # strategy which depends on the operating system
+        self.assertIn(f'at position '.encode(), proc.err)
+
 
 @unittest.skipIf(ctypes is None, 'need ctypes')
 class PyUnicodeWriterFormatTest(unittest.TestCase):
diff --git 
a/Misc/NEWS.d/next/C_API/2026-09-18-00-29-49.gh-issue-157710.Q_AD8D.rst 
b/Misc/NEWS.d/next/C_API/2026-09-18-00-29-49.gh-issue-157710.Q_AD8D.rst
new file mode 100644
index 000000000000000..532978485af85a5
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-09-18-00-29-49.gh-issue-157710.Q_AD8D.rst
@@ -0,0 +1,3 @@
+When Python is built in debug mode, :c:func:`PyUnicodeWriter_Finish` now
+checks if the trailing null byte has been overridden to detect buffer
+overflow. Patch by Victor Stinner.
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 38e56ae70420985..d30affdbf621392 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -3206,6 +3206,28 @@ 
test_thread_state_ensure_from_view_interp_switch(PyObject *self, PyObject *unuse
     Py_RETURN_NONE;
 }
 
+static PyObject *
+unicodewriter_overflow(PyObject *self, PyObject *unused)
+{
+    PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
+    if (writer == NULL) {
+        return NULL;
+    }
+    if (PyUnicodeWriter_WriteASCII(writer, "hello", -1) < 0) {
+        PyUnicodeWriter_Discard(writer);
+        return NULL;
+    }
+
+    _PyUnicodeWriter *impl = (_PyUnicodeWriter*)writer;
+    PyObject *buffer = impl->buffer;
+    Py_ssize_t index = PyUnicode_GET_LENGTH(buffer);
+    PyUnicode_WRITE(impl->kind, impl->data, index, '#');  // overflow!
+
+    // Spoiler: the function doesn't return if an overflow is detected
+    // in debug mode
+    return PyUnicodeWriter_Finish(writer);
+}
+
 /* Self interrupting context manager */
 
 typedef struct {
@@ -3393,6 +3415,7 @@ static PyMethodDef module_functions[] = {
     {"test_interp_guard_countdown", test_interp_guard_countdown, METH_NOARGS},
     {"test_interp_view_countdown", test_interp_view_countdown, METH_NOARGS},
     {"test_thread_state_ensure_from_view_interp_switch", 
test_thread_state_ensure_from_view_interp_switch, METH_NOARGS},
+    {"unicodewriter_overflow", unicodewriter_overflow, METH_NOARGS},
     {NULL, NULL} /* sentinel */
 };
 
diff --git a/Objects/unicode_writer.c b/Objects/unicode_writer.c
index b10d9e94098935a..fe1bd97775b3ae2 100644
--- a/Objects/unicode_writer.c
+++ b/Objects/unicode_writer.c
@@ -608,6 +608,20 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
 {
     PyObject *str;
 
+#ifdef Py_DEBUG
+    // Check for buffer overflow
+    if (writer->buffer != NULL) {
+        Py_ssize_t pos = PyUnicode_GET_LENGTH(writer->buffer);
+        Py_UCS4 ch = PyUnicode_READ_CHAR(writer->buffer, pos);
+        if (ch != 0) {
+            _Py_FatalErrorFormat(__func__,
+                                 "Buffer overflow detected in "
+                                 "PyUnicodeWriter %p at position %zd",
+                                 writer, pos);
+        }
+    }
+#endif
+
     if (writer->pos == 0) {
         Py_CLEAR(writer->buffer);
         return _PyUnicode_GetEmpty();
@@ -618,6 +632,7 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
 
     if (writer->readonly) {
         assert(PyUnicode_GET_LENGTH(str) == writer->pos);
+        assert(_PyUnicode_CheckConsistency(str, 1));
         return str;
     }
 
diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c
index 1e687e7a36a8818..7ee9d17b7f77ff2 100644
--- a/Objects/unicodeobject.c
+++ b/Objects/unicodeobject.c
@@ -601,7 +601,6 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
 # define CHECK_IF_FT(expr) (void)(expr)
 #endif
 
-
     assert(op != NULL);
     CHECK(PyUnicode_Check(op));
 
@@ -647,13 +646,12 @@ _PyUnicode_CheckConsistency(PyObject *op, int 
check_content)
     }
 
     /* check that the best kind is used: O(n) operation */
+    const void *data = PyUnicode_DATA(ascii);
     if (check_content) {
         Py_ssize_t i;
         Py_UCS4 maxchar = 0;
-        const void *data;
         Py_UCS4 ch;
 
-        data = PyUnicode_DATA(ascii);
         for (i=0; i < ascii->length; i++)
         {
             ch = PyUnicode_READ(kind, data, i);
@@ -676,9 +674,12 @@ _PyUnicode_CheckConsistency(PyObject *op, int 
check_content)
             CHECK(maxchar >= 0x10000);
             CHECK(maxchar <= MAX_UNICODE);
         }
-        CHECK(PyUnicode_READ(kind, data, ascii->length) == 0);
     }
 
+    // Detect buffer overflow: check if the trailing null character
+    // has been overridden
+    CHECK(PyUnicode_READ(kind, data, ascii->length) == 0);
+
     /* Check interning state */
 #ifdef Py_DEBUG
     // Note that we do not check `_Py_IsImmortal(op)` in the GIL-enabled build

_______________________________________________
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