Skip to content

Commit 2385254

Browse files
authored
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.
1 parent 8542958 commit 2385254

5 files changed

Lines changed: 65 additions & 5 deletions

File tree

‎Lib/test/test_capi/test_unicode.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import unittest
21
import sys
2+
import textwrap
3+
import unittest
34
from test import support
45
from test.support import threading_helper
6+
from test.support.script_helper import assert_python_failure
57

68
try:
79
import _testcapi
@@ -1992,6 +1994,22 @@ def test_singletons(self):
19921994
writer.write_substring(ch + 'xxx', 0, 1)
19931995
self.assertIs(writer.finish(), ch)
19941996

1997+
@unittest.skipUnless(support.Py_DEBUG, 'need debug build (Py_DEBUG)')
1998+
def test_detect_overflow(self):
1999+
# Test detection of buffer overflow
2000+
code = textwrap.dedent('''
2001+
from test.support import SuppressCrashReport
2002+
import _testinternalcapi
2003+
2004+
SuppressCrashReport().__enter__()
2005+
_testinternalcapi.unicodewriter_overflow()
2006+
''')
2007+
proc = assert_python_failure('-c', code)
2008+
self.assertIn(b'Buffer overflow detected in PyUnicodeWriter', proc.err)
2009+
# Do not test the position value since it depends on the overallocation
2010+
# strategy which depends on the operating system
2011+
self.assertIn(f'at position '.encode(), proc.err)
2012+
19952013

19962014
@unittest.skipIf(ctypes is None, 'need ctypes')
19972015
class PyUnicodeWriterFormatTest(unittest.TestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When Python is built in debug mode, :c:func:`PyUnicodeWriter_Finish` now
2+
checks if the trailing null byte has been overridden to detect buffer
3+
overflow. Patch by Victor Stinner.

‎Modules/_testinternalcapi.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3206,6 +3206,28 @@ test_thread_state_ensure_from_view_interp_switch(PyObject *self, PyObject *unuse
32063206
Py_RETURN_NONE;
32073207
}
32083208

3209+
static PyObject *
3210+
unicodewriter_overflow(PyObject *self, PyObject *unused)
3211+
{
3212+
PyUnicodeWriter *writer = PyUnicodeWriter_Create(0);
3213+
if (writer == NULL) {
3214+
return NULL;
3215+
}
3216+
if (PyUnicodeWriter_WriteASCII(writer, "hello", -1) < 0) {
3217+
PyUnicodeWriter_Discard(writer);
3218+
return NULL;
3219+
}
3220+
3221+
_PyUnicodeWriter *impl = (_PyUnicodeWriter*)writer;
3222+
PyObject *buffer = impl->buffer;
3223+
Py_ssize_t index = PyUnicode_GET_LENGTH(buffer);
3224+
PyUnicode_WRITE(impl->kind, impl->data, index, '#'); // overflow!
3225+
3226+
// Spoiler: the function doesn't return if an overflow is detected
3227+
// in debug mode
3228+
return PyUnicodeWriter_Finish(writer);
3229+
}
3230+
32093231
/* Self interrupting context manager */
32103232

32113233
typedef struct {
@@ -3393,6 +3415,7 @@ static PyMethodDef module_functions[] = {
33933415
{"test_interp_guard_countdown", test_interp_guard_countdown, METH_NOARGS},
33943416
{"test_interp_view_countdown", test_interp_view_countdown, METH_NOARGS},
33953417
{"test_thread_state_ensure_from_view_interp_switch", test_thread_state_ensure_from_view_interp_switch, METH_NOARGS},
3418+
{"unicodewriter_overflow", unicodewriter_overflow, METH_NOARGS},
33963419
{NULL, NULL} /* sentinel */
33973420
};
33983421

‎Objects/unicode_writer.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,20 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
608608
{
609609
PyObject *str;
610610

611+
#ifdef Py_DEBUG
612+
// Check for buffer overflow
613+
if (writer->buffer != NULL) {
614+
Py_ssize_t pos = PyUnicode_GET_LENGTH(writer->buffer);
615+
Py_UCS4 ch = PyUnicode_READ_CHAR(writer->buffer, pos);
616+
if (ch != 0) {
617+
_Py_FatalErrorFormat(__func__,
618+
"Buffer overflow detected in "
619+
"PyUnicodeWriter %p at position %zd",
620+
writer, pos);
621+
}
622+
}
623+
#endif
624+
611625
if (writer->pos == 0) {
612626
Py_CLEAR(writer->buffer);
613627
return _PyUnicode_GetEmpty();
@@ -618,6 +632,7 @@ _PyUnicodeWriter_Finish(_PyUnicodeWriter *writer)
618632

619633
if (writer->readonly) {
620634
assert(PyUnicode_GET_LENGTH(str) == writer->pos);
635+
assert(_PyUnicode_CheckConsistency(str, 1));
621636
return str;
622637
}
623638

‎Objects/unicodeobject.c‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,6 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
601601
# define CHECK_IF_FT(expr) (void)(expr)
602602
#endif
603603

604-
605604
assert(op != NULL);
606605
CHECK(PyUnicode_Check(op));
607606

@@ -647,13 +646,12 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
647646
}
648647

649648
/* check that the best kind is used: O(n) operation */
649+
const void *data = PyUnicode_DATA(ascii);
650650
if (check_content) {
651651
Py_ssize_t i;
652652
Py_UCS4 maxchar = 0;
653-
const void *data;
654653
Py_UCS4 ch;
655654

656-
data = PyUnicode_DATA(ascii);
657655
for (i=0; i < ascii->length; i++)
658656
{
659657
ch = PyUnicode_READ(kind, data, i);
@@ -676,9 +674,12 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
676674
CHECK(maxchar >= 0x10000);
677675
CHECK(maxchar <= MAX_UNICODE);
678676
}
679-
CHECK(PyUnicode_READ(kind, data, ascii->length) == 0);
680677
}
681678

679+
// Detect buffer overflow: check if the trailing null character
680+
// has been overridden
681+
CHECK(PyUnicode_READ(kind, data, ascii->length) == 0);
682+
682683
/* Check interning state */
683684
#ifdef Py_DEBUG
684685
// Note that we do not check `_Py_IsImmortal(op)` in the GIL-enabled build

0 commit comments

Comments
 (0)