Skip to content

Commit 89a8ac9

Browse files
committed
gh-156939: Detect buffer overflow in PyBytesWriter in debug mode
Reserve one byte in PyBytesWriter used as a canary byte: set it to a special value. PyBytesWriter_Finish() checks if the canary byte has been overriden. Add a test on the feature. Update buffer overflow check in fcntl: allocate extra guard bytes in the writer and then truncate these bytes.
1 parent 9398655 commit 89a8ac9

5 files changed

Lines changed: 121 additions & 8 deletions

File tree

Lib/test/test_capi/test_bytes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import textwrap
12
import unittest
23
from test.support import import_helper
4+
from test.support.script_helper import assert_python_failure
35

46
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
57
_testcapi = import_helper.import_module('_testcapi')
@@ -430,6 +432,25 @@ def test_example_resize(self):
430432
def test_example_highlevel(self):
431433
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
432434

435+
def test_canary_byte(self):
436+
small_buffer = _testcapi.PyBytesWriter_small_buffer
437+
large_size = small_buffer * 10
438+
439+
# Test small buffer and large buffer
440+
for size in (0, 3, large_size):
441+
with self.subTest(size=size):
442+
code = textwrap.dedent(f"""
443+
from test.support import SuppressCrashReport
444+
import _testcapi
445+
size = {size}
446+
data = b'x' * size
447+
with SuppressCrashReport():
448+
_testcapi.byteswriter_test_canary_byte(data)
449+
""")
450+
proc = assert_python_failure('-c', code)
451+
self.assertIn(b'Buffer overflow detected in PyBytesWriter',
452+
proc.err)
453+
433454

434455
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
435456
result_type = bytearray
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When Python is built in debug mode, :c:type:`PyBytesWriter` now detects
2+
buffer overflow. Patch by Victor Stinner.

Modules/_testcapi/bytes.c

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ writer_write_bytes(PyObject *self_raw, PyObject *args)
151151
return NULL;
152152
}
153153

154-
char *bytes;
154+
const char *bytes;
155155
Py_ssize_t unused_size, size;
156156
if (!PyArg_ParseTuple(args, "y#n", &bytes, &unused_size, &size)) {
157157
return NULL;
@@ -454,13 +454,46 @@ test_byteswriter_ptr(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
454454
}
455455

456456

457+
// Trigger a buffer overflow on purpose to test the canary byte feature
458+
// which detects buffer overflow
459+
static PyObject *
460+
byteswriter_test_canary_byte(PyObject *Py_UNUSED(module), PyObject *args)
461+
{
462+
const char *str;
463+
Py_ssize_t len;
464+
if (!PyArg_ParseTuple(args, "s#", &str, &len)) {
465+
return NULL;
466+
}
467+
468+
PyBytesWriter *writer = PyBytesWriter_Create(0);
469+
if (writer == NULL) {
470+
goto error;
471+
}
472+
if (PyBytesWriter_Grow(writer, len) < 0) {
473+
goto error;
474+
}
475+
char *data = PyBytesWriter_GetData(writer);
476+
if (len) {
477+
memcpy(data, str, len);
478+
}
479+
data[len] = '#'; // Overflow!
480+
481+
return PyBytesWriter_Finish(writer);
482+
483+
error:
484+
PyBytesWriter_Discard(writer);
485+
return NULL;
486+
}
487+
488+
457489
static PyMethodDef test_methods[] = {
458490
{"bytes_resize", bytes_resize, METH_VARARGS},
459491
{"bytes_join", bytes_join, METH_VARARGS},
460492
{"byteswriter_abc", byteswriter_abc, METH_NOARGS},
461493
{"byteswriter_resize", byteswriter_resize, METH_NOARGS},
462494
{"byteswriter_highlevel", byteswriter_highlevel, METH_NOARGS},
463495
{"test_byteswriter_ptr", test_byteswriter_ptr, METH_NOARGS},
496+
{"byteswriter_test_canary_byte", byteswriter_test_canary_byte, METH_VARARGS},
464497
{NULL},
465498
};
466499

Modules/fcntlmodule.c

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,14 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
121121
return PyBytes_FromStringAndSize(buf, len);
122122
}
123123
else {
124-
PyBytesWriter *writer = PyBytesWriter_Create(len);
124+
PyBytesWriter *writer = PyBytesWriter_Create(len + GUARDSZ);
125125
if (writer == NULL) {
126126
PyBuffer_Release(&view);
127127
return NULL;
128128
}
129129
char *ptr = PyBytesWriter_GetData(writer);
130130
memcpy(ptr, view.buf, len);
131+
memcpy(ptr + len, guard, GUARDSZ);
131132
PyBuffer_Release(&view);
132133

133134
do {
@@ -142,7 +143,7 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
142143
PyBytesWriter_Discard(writer);
143144
return NULL;
144145
}
145-
if (ptr[len] != '\0') {
146+
if (memcmp(ptr + len, guard, GUARDSZ) != 0) {
146147
PyErr_SetString(PyExc_SystemError,
147148
"Memory corruption in fcntl() due to "
148149
"buffer overflow. "
@@ -151,7 +152,8 @@ fcntl_fcntl_impl(PyObject *module, int fd, int code, PyObject *arg)
151152
PyBytesWriter_Discard(writer);
152153
return NULL;
153154
}
154-
return PyBytesWriter_Finish(writer);
155+
// Truncate the trailing guard bytes
156+
return PyBytesWriter_FinishWithSize(writer, len);
155157
}
156158
#undef FCNTL_BUFSZ
157159
}
@@ -316,13 +318,14 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
316318
return PyBytes_FromStringAndSize(buf, len);
317319
}
318320
else {
319-
PyBytesWriter *writer = PyBytesWriter_Create(len);
321+
PyBytesWriter *writer = PyBytesWriter_Create(len + GUARDSZ);
320322
if (writer == NULL) {
321323
PyBuffer_Release(&view);
322324
return NULL;
323325
}
324326
char *ptr = PyBytesWriter_GetData(writer);
325327
memcpy(ptr, view.buf, len);
328+
memcpy(ptr + len, guard, GUARDSZ);
326329
PyBuffer_Release(&view);
327330

328331
do {
@@ -337,7 +340,7 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
337340
PyBytesWriter_Discard(writer);
338341
return NULL;
339342
}
340-
if (ptr[len] != '\0') {
343+
if (memcmp(ptr + len, guard, GUARDSZ) != 0) {
341344
PyErr_SetString(PyExc_SystemError,
342345
"Memory corruption in ioctl() due to "
343346
"buffer overflow. "
@@ -346,7 +349,8 @@ fcntl_ioctl_impl(PyObject *module, int fd, unsigned long code, PyObject *arg,
346349
PyBytesWriter_Discard(writer);
347350
return NULL;
348351
}
349-
return PyBytesWriter_Finish(writer);
352+
// Truncate the trailing guard bytes
353+
return PyBytesWriter_FinishWithSize(writer, len);
350354
}
351355
#undef IOCTL_BUFSZ
352356
}

Objects/bytesobject.c

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3593,6 +3593,10 @@ _PyBytes_RepeatBuffer(char* dest, Py_ssize_t len_dest,
35933593

35943594
// --- PyBytesWriter API -----------------------------------------------------
35953595

3596+
// Use a value different than NUL (0) to be able to detect overflow writing
3597+
// one extra NUL byte which is a common error.
3598+
#define PyBytesWriter_CANARY_BYTE PYMEM_DEADBYTE
3599+
35963600
static inline char*
35973601
byteswriter_data(PyBytesWriter *writer)
35983602
{
@@ -3604,7 +3608,8 @@ static inline Py_ssize_t
36043608
byteswriter_allocated(PyBytesWriter *writer)
36053609
{
36063610
if (writer->obj == NULL) {
3607-
return sizeof(writer->small_buffer);
3611+
// Reserve the last byte for the canary byte
3612+
return sizeof(writer->small_buffer) - 1;
36083613
}
36093614
else if (writer->use_bytearray) {
36103615
return PyByteArray_GET_SIZE(writer->obj);
@@ -3615,6 +3620,31 @@ byteswriter_allocated(PyBytesWriter *writer)
36153620
}
36163621

36173622

3623+
#ifdef Py_DEBUG
3624+
static void
3625+
byteswriter_check_canary_byte(PyBytesWriter *writer)
3626+
{
3627+
const unsigned char *data = (const unsigned char*)byteswriter_data(writer);
3628+
unsigned char canary = data[writer->size];
3629+
if (canary != PyBytesWriter_CANARY_BYTE) {
3630+
_Py_FatalErrorFormat(__func__,
3631+
"Buffer overflow detected in PyBytesWriter %p: "
3632+
"one byte written after the buffer "
3633+
"(at position %zd)",
3634+
writer, writer->size);
3635+
}
3636+
}
3637+
3638+
3639+
static void
3640+
byteswriter_write_canary_byte(PyBytesWriter *writer)
3641+
{
3642+
unsigned char *data = (unsigned char*)byteswriter_data(writer);
3643+
data[writer->size] = PyBytesWriter_CANARY_BYTE;
3644+
}
3645+
#endif
3646+
3647+
36183648
#ifdef MS_WINDOWS
36193649
/* On Windows, overallocate by 50% is the best factor */
36203650
# define OVERALLOCATE_FACTOR 2
@@ -3719,6 +3749,7 @@ byteswriter_create(Py_ssize_t size, int use_bytearray)
37193749
}
37203750
#ifdef Py_DEBUG
37213751
memset(byteswriter_data(writer), 0xff, byteswriter_allocated(writer));
3752+
byteswriter_write_canary_byte(writer);
37223753
#endif
37233754
return writer;
37243755
}
@@ -3764,6 +3795,19 @@ PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
37643795
goto error;
37653796
}
37663797

3798+
#ifdef Py_DEBUG
3799+
// Check for buffer overflow
3800+
byteswriter_check_canary_byte(writer);
3801+
3802+
if (writer->obj != NULL) {
3803+
// byteswriter_write_canary_byte() can override the trailing NUL byte.
3804+
// So reset the trailing NUL byte to NUL.
3805+
Py_ssize_t allocated = byteswriter_allocated(writer);
3806+
char *data = byteswriter_data(writer);
3807+
data[allocated] = '\0';
3808+
}
3809+
#endif
3810+
37673811
PyObject *result;
37683812
if (size == 0) {
37693813
result = bytes_get_empty();
@@ -3850,6 +3894,9 @@ PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
38503894
return -1;
38513895
}
38523896
writer->size = size;
3897+
#ifdef Py_DEBUG
3898+
byteswriter_write_canary_byte(writer);
3899+
#endif
38533900
return 0;
38543901
}
38553902

@@ -3883,6 +3930,9 @@ PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size)
38833930
return -1;
38843931
}
38853932
writer->size = size;
3933+
#ifdef Py_DEBUG
3934+
byteswriter_write_canary_byte(writer);
3935+
#endif
38863936
return 0;
38873937
}
38883938

@@ -3948,5 +3998,8 @@ _PyBytesWriter_ResizeToAllocated(PyBytesWriter *writer)
39483998
{
39493999
Py_ssize_t allocated = byteswriter_allocated(writer);
39504000
writer->size = allocated;
4001+
#ifdef Py_DEBUG
4002+
byteswriter_write_canary_byte(writer);
4003+
#endif
39514004
return allocated;
39524005
}

0 commit comments

Comments
 (0)