Skip to content

Commit 97c29ff

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. Tests: only run the 3 "example" tests in BytesWriterTest. There is no need to rerun them in ByteArrayWriterTest.
1 parent aaae15c commit 97c29ff

4 files changed

Lines changed: 144 additions & 10 deletions

File tree

Lib/test/test_capi/test_bytes.py

Lines changed: 30 additions & 9 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')
@@ -378,15 +380,6 @@ def test_format_i(self):
378380
writer.format_i(b'y=%i', 456)
379381
self.assertEqual(writer.finish(), self.result_type(b'x=123, y=456'))
380382

381-
def test_example_abc(self):
382-
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
383-
384-
def test_example_resize(self):
385-
self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
386-
387-
def test_example_highlevel(self):
388-
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
389-
390383

391384
class BytesWriterTest(BaseWriterTest, unittest.TestCase):
392385
result_type = bytes
@@ -424,6 +417,34 @@ def test_singletons(self):
424417
writer.write_bytes(unused_text, len(unused_text))
425418
self.assertIs(writer.finish_with_size(1), singletons[ch])
426419

420+
def test_example_abc(self):
421+
self.assertEqual(_testcapi.byteswriter_abc(), b'abc')
422+
423+
def test_example_resize(self):
424+
self.assertEqual(_testcapi.byteswriter_resize(), b'Hello World')
425+
426+
def test_example_highlevel(self):
427+
self.assertEqual(_testcapi.byteswriter_highlevel(), b'Hello World!')
428+
429+
def test_canary_byte(self):
430+
small_buffer = _testcapi.PyBytesWriter_small_buffer
431+
large_size = small_buffer * 10
432+
433+
# Test small buffer and large buffer
434+
for size in (0, 3, large_size):
435+
with self.subTest(size=size):
436+
code = textwrap.dedent(f"""
437+
from test.support import SuppressCrashReport
438+
import _testcapi
439+
size = {size}
440+
data = b'x' * size
441+
with SuppressCrashReport():
442+
_testcapi.byteswriter_test_canary_byte(data)
443+
""")
444+
proc = assert_python_failure('-c', code)
445+
self.assertIn(b'Buffer overflow detected in PyBytesWriter',
446+
proc.err)
447+
427448

428449
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
429450
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;
@@ -353,12 +353,45 @@ byteswriter_highlevel(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
353353
}
354354

355355

356+
// Trigger a buffer overflow on purpose to test the canary byte feature
357+
// which detects buffer overflow
358+
static PyObject *
359+
byteswriter_test_canary_byte(PyObject *Py_UNUSED(module), PyObject *args)
360+
{
361+
const char *str;
362+
Py_ssize_t len;
363+
if (!PyArg_ParseTuple(args, "s#", &str, &len)) {
364+
return NULL;
365+
}
366+
367+
PyBytesWriter *writer = PyBytesWriter_Create(0);
368+
if (writer == NULL) {
369+
goto error;
370+
}
371+
if (PyBytesWriter_Grow(writer, len) < 0) {
372+
goto error;
373+
}
374+
char *data = PyBytesWriter_GetData(writer);
375+
if (len) {
376+
memcpy(data, str, len);
377+
}
378+
data[len] = '#'; // Overflow!
379+
380+
return PyBytesWriter_Finish(writer);
381+
382+
error:
383+
PyBytesWriter_Discard(writer);
384+
return NULL;
385+
}
386+
387+
356388
static PyMethodDef test_methods[] = {
357389
{"bytes_resize", bytes_resize, METH_VARARGS},
358390
{"bytes_join", bytes_join, METH_VARARGS},
359391
{"byteswriter_abc", byteswriter_abc, METH_NOARGS},
360392
{"byteswriter_resize", byteswriter_resize, METH_NOARGS},
361393
{"byteswriter_highlevel", byteswriter_highlevel, METH_NOARGS},
394+
{"byteswriter_test_canary_byte", byteswriter_test_canary_byte, METH_VARARGS},
362395
{NULL},
363396
};
364397

Objects/bytesobject.c

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3589,6 +3589,10 @@ _PyBytes_RepeatBuffer(char* dest, Py_ssize_t len_dest,
35893589

35903590
// --- PyBytesWriter API -----------------------------------------------------
35913591

3592+
// Use a value different than NUL (0) to be able to detect overflow writing
3593+
// one extra NUL byte which is a common error.
3594+
#define PyBytesWriter_CANARY_BYTE PYMEM_DEADBYTE
3595+
35923596
static inline char*
35933597
byteswriter_data(PyBytesWriter *writer)
35943598
{
@@ -3611,6 +3615,49 @@ byteswriter_allocated(PyBytesWriter *writer)
36113615
}
36123616

36133617

3618+
#ifdef Py_DEBUG
3619+
static int
3620+
byteswriter_has_canary_byte(PyBytesWriter *writer)
3621+
{
3622+
if (writer->obj == NULL) {
3623+
return (writer->size < byteswriter_allocated(writer));
3624+
}
3625+
else {
3626+
// For bytes or bytearray, use the trailing NUL byte
3627+
// as the canary byte.
3628+
return 1;
3629+
}
3630+
}
3631+
3632+
3633+
static void
3634+
byteswriter_check_canary_byte(PyBytesWriter *writer)
3635+
{
3636+
assert(byteswriter_has_canary_byte(writer));
3637+
3638+
const unsigned char *data = (const unsigned char*)byteswriter_data(writer);
3639+
unsigned char canary = data[writer->size];
3640+
if (canary != PyBytesWriter_CANARY_BYTE) {
3641+
_Py_FatalErrorFormat(__func__,
3642+
"Buffer overflow detected in PyBytesWriter %p: "
3643+
"one byte written after the buffer "
3644+
"(at position %zd)",
3645+
writer, writer->size);
3646+
}
3647+
}
3648+
3649+
3650+
static void
3651+
byteswriter_write_canary_byte(PyBytesWriter *writer)
3652+
{
3653+
assert(byteswriter_has_canary_byte(writer));
3654+
3655+
unsigned char *data = (unsigned char*)byteswriter_data(writer);
3656+
data[writer->size] = PyBytesWriter_CANARY_BYTE;
3657+
}
3658+
#endif
3659+
3660+
36143661
#ifdef MS_WINDOWS
36153662
/* On Windows, overallocate by 50% is the best factor */
36163663
# define OVERALLOCATE_FACTOR 2
@@ -3625,6 +3672,17 @@ byteswriter_resize(PyBytesWriter *writer, Py_ssize_t size, int resize)
36253672
assert(size >= 0);
36263673

36273674
Py_ssize_t old_allocated = byteswriter_allocated(writer);
3675+
#ifdef Py_DEBUG
3676+
if (writer->obj == NULL) {
3677+
// If the small_buffer is used, reserve one byte for the canary byte.
3678+
if (size <= (old_allocated - 1)) {
3679+
return 0;
3680+
}
3681+
}
3682+
else
3683+
// bytes and bytearray always allocates one extra byte for a trailing
3684+
// NUL byte: use this byte as the canary byte.
3685+
#endif
36283686
if (size <= old_allocated) {
36293687
return 0;
36303688
}
@@ -3713,9 +3771,12 @@ byteswriter_create(Py_ssize_t size, int use_bytearray)
37133771
}
37143772
writer->size = size;
37153773
}
3774+
37163775
#ifdef Py_DEBUG
37173776
memset(byteswriter_data(writer), 0xff, byteswriter_allocated(writer));
3777+
byteswriter_write_canary_byte(writer);
37183778
#endif
3779+
37193780
return writer;
37203781
}
37213782

@@ -3747,6 +3808,17 @@ PyBytesWriter_Discard(PyBytesWriter *writer)
37473808
PyObject*
37483809
PyBytesWriter_FinishWithSize(PyBytesWriter *writer, Py_ssize_t size)
37493810
{
3811+
#ifdef Py_DEBUG
3812+
byteswriter_check_canary_byte(writer);
3813+
if (writer->obj != NULL) {
3814+
// byteswriter_write_canary_byte() can override the trailing NUL byte.
3815+
// So reset the trailing NUL byte to NUL.
3816+
Py_ssize_t allocated = byteswriter_allocated(writer);
3817+
char *data = byteswriter_data(writer);
3818+
data[allocated] = '\0';
3819+
}
3820+
#endif
3821+
37503822
PyObject *result;
37513823
if (size == 0) {
37523824
result = bytes_get_empty();
@@ -3846,6 +3918,9 @@ PyBytesWriter_Resize(PyBytesWriter *writer, Py_ssize_t size)
38463918
return -1;
38473919
}
38483920
writer->size = size;
3921+
#ifdef Py_DEBUG
3922+
byteswriter_write_canary_byte(writer);
3923+
#endif
38493924
return 0;
38503925
}
38513926

@@ -3879,6 +3954,9 @@ PyBytesWriter_Grow(PyBytesWriter *writer, Py_ssize_t size)
38793954
return -1;
38803955
}
38813956
writer->size = size;
3957+
#ifdef Py_DEBUG
3958+
byteswriter_write_canary_byte(writer);
3959+
#endif
38823960
return 0;
38833961
}
38843962

0 commit comments

Comments
 (0)