11import sys
2+ import textwrap
23import unittest
34from test import support
45from test .support import import_helper
6+ from test .support .script_helper import assert_python_failure
57
68_testlimitedcapi = import_helper .import_module ('_testlimitedcapi' )
79_testcapi = import_helper .import_module ('_testcapi' )
@@ -316,12 +318,18 @@ def test_join(self):
316318 bytes_join (b'' , NULL )
317319
318320
321+ def get_data_canary (writer ):
322+ size = writer .get_size () + 1
323+ return writer .get_data (size )
324+
325+
319326class BaseWriterTest :
320327 RESULT_TYPE = NotImplementedError
321328 SMALL_BUFFER = 11 # bytes
322329 assert SMALL_BUFFER < _testcapi .PyBytesWriter_small_buffer
323330 LARGE_BUFFER = _testcapi .PyBytesWriter_small_buffer + 17 # bytes
324331 NEW_BYTE = b'\xff '
332+ CANARY_BYTE = b'\xdd '
325333
326334 def create_writer (self , alloc = 0 , string = b'' ):
327335 raise NotImplementedError
@@ -344,6 +352,7 @@ def test_get_data(self):
344352 # Test PyBytesWriter_GetData()
345353 writer = self .create_writer (6 )
346354 NEW_BYTE = self .NEW_BYTE
355+ CANARY_BYTE = self .CANARY_BYTE
347356 self .assertEqual (writer .get_data (), NEW_BYTE * 6 )
348357 writer .write (0 , b'abc' )
349358 self .assertEqual (writer .get_data (), b'abc' + NEW_BYTE * 3 )
@@ -357,7 +366,7 @@ def test_get_data(self):
357366 writer .write (0 , b's' * small )
358367 self .assertEqual (writer .get_data (), b's' * small )
359368 writer .resize (large )
360- self .assertEqual (writer .get_data (), b's' * small + NEW_BYTE * (large - small ))
369+ self .assertEqual (writer .get_data (), b's' * small + CANARY_BYTE + NEW_BYTE * (large - small - 1 ))
361370 writer .write (small , b'L' * (large - small ))
362371 self .assertEqual (writer .get_data (), b's' * small + b'L' * (large - small ))
363372
@@ -443,6 +452,47 @@ def test_resize(self):
443452 writer .resize (_testcapi .PY_SSIZE_T_MAX )
444453 self .assertEqual (writer .finish (), b'x' * size )
445454
455+ @unittest .skipUnless (support .Py_DEBUG , 'need debug build' )
456+ def test_resize_canary (self ):
457+ CANARY_BYTE = self .CANARY_BYTE
458+ for size in (self .SMALL_BUFFER , self .LARGE_BUFFER ):
459+ with self .subTest (size = size ):
460+ # Truncate the last byte
461+ data = b'x' * size
462+ writer = self .create_writer (size )
463+ writer .write (0 , data )
464+ self .assertEqual (get_data_canary (writer ), data + CANARY_BYTE )
465+ writer .resize (size - 1 )
466+ self .assertEqual (get_data_canary (writer ), data [:- 1 ] + CANARY_BYTE )
467+ self .assertEqual (writer .finish (), data [:- 1 ])
468+
469+ # Make the buffer empty
470+ writer = self .create_writer (size )
471+ writer .write (0 , data )
472+ writer .resize (0 )
473+ self .assertEqual (writer .get_data (), b'' )
474+ self .assertEqual (writer .finish (), b'' )
475+
476+ @support .nomemtest
477+ def test_resize_error (self ):
478+ # Test PyBytesWriter_Resize() error
479+ init = b'x' * self .LARGE_BUFFER
480+ writer = self .create_writer (len (init ))
481+ writer .write (0 , init )
482+ size = len (init ) + 100
483+ try :
484+ with self .assertRaises (MemoryError ):
485+ _testcapi .set_nomemory (0 )
486+ writer .resize (size )
487+ finally :
488+ _testcapi .remove_mem_hooks ()
489+ suffix = b'still working'
490+ writer .write_bytes (suffix , - 1 )
491+ self .assertEqual (writer .finish (), init + suffix )
492+
493+ # Note: PyBytesWriter_Resize() leaves the buffer unchanged (no resize)
494+ # if the new size is smaller than the allocated size
495+
446496 def test_grow (self ):
447497 # Test PyBytesWriter_Grow()
448498 writer = self .create_writer (0 )
@@ -476,31 +526,51 @@ def test_grow(self):
476526 with self .subTest (size = size ):
477527 writer = self .create_writer ()
478528 writer .write_bytes (b'x' * size , - 1 )
479- with self .assertRaisesRegex (ValueError , 'size must be >= 0 ' ):
480- writer .grow (- 1 )
529+ with self .assertRaisesRegex (ValueError , 'invalid size ' ):
530+ writer .grow (- size - 1 )
481531 with self .assertRaises (MemoryError ):
482532 writer .grow (_testcapi .PY_SSIZE_T_MAX )
483533 self .assertEqual (writer .finish (), b'x' * size )
484534
535+ @unittest .skipUnless (support .Py_DEBUG , 'need debug build' )
536+ def test_grow_canary (self ):
537+ CANARY_BYTE = self .CANARY_BYTE
538+ for size in (self .SMALL_BUFFER , self .LARGE_BUFFER ):
539+ with self .subTest (size = size ):
540+ # Truncate the last byte
541+ data = b'x' * size
542+ writer = self .create_writer (size )
543+ writer .write (0 , data )
544+ self .assertEqual (get_data_canary (writer ), data + CANARY_BYTE )
545+ writer .grow (- 1 )
546+ self .assertEqual (get_data_canary (writer ), data [:- 1 ] + CANARY_BYTE )
547+ self .assertEqual (writer .finish (), data [:- 1 ])
548+
549+ # Make the buffer empty
550+ writer = self .create_writer (size )
551+ writer .write (0 , data )
552+ writer .grow (- size )
553+ self .assertEqual (writer .get_data (), b'' )
554+ self .assertEqual (writer .finish (), b'' )
555+
485556 @support .nomemtest
486- def test_resize_error (self ):
487- # Test PyBytesWriter_Resize () error
557+ def test_grow_error (self ):
558+ # Test PyBytesWriter_Grow () error
488559 init = b'x' * self .LARGE_BUFFER
489560 writer = self .create_writer (len (init ))
490561 writer .write (0 , init )
491- size = len (init ) + 100
492562 try :
493563 with self .assertRaises (MemoryError ):
494564 _testcapi .set_nomemory (0 )
495- writer .resize ( size )
565+ writer .grow ( 100 )
496566 finally :
497567 _testcapi .remove_mem_hooks ()
498568 suffix = b'still working'
499569 writer .write_bytes (suffix , - 1 )
500570 self .assertEqual (writer .finish (), init + suffix )
501571
502- # Note: PyBytesWriter_Resize () leaves the buffer unchanged (no resize)
503- # if the new size is smaller than the allocated size
572+ # Note: PyBytesWriter_Grow () leaves the buffer unchanged (no resize)
573+ # if grow is negative.
504574
505575 def test_format_i (self ):
506576 # Test PyBytesWriter_Format()
@@ -513,6 +583,67 @@ def test_format_i(self):
513583 writer .format_i (b'y=%i' , 456 )
514584 self .assertEqual (writer .finish (), b'x=123, y=456' )
515585
586+ @unittest .skipUnless (support .Py_DEBUG , 'need a Python debug build' )
587+ def test_canary_byte (self ):
588+ small_buffer = _testcapi .PyBytesWriter_small_buffer
589+ large_size = small_buffer * 10
590+ use_bytearray = (self .RESULT_TYPE == bytearray )
591+
592+ # Test small buffer and large buffer
593+ for size in (0 , self .SMALL_BUFFER , self .LARGE_BUFFER ):
594+ for operation in (
595+ 'writer.get_data()' ,
596+ 'writer.get_size()' ,
597+ f'writer.resize({ size } * 2)' ,
598+ f'writer.grow({ size } )' ,
599+ 'writer.discard()' ,
600+ 'writer.finish()' ,
601+ ):
602+ with self .subTest (size = size , operation = operation ):
603+ code = textwrap .dedent (f"""
604+ from test.support import SuppressCrashReport
605+ import os
606+ import _testcapi
607+ size = { size }
608+ # Add an extra '#' byte to trigger a buffer overflow
609+ data = b'x' * size + b'#'
610+ use_bytearray = { use_bytearray }
611+ writer = _testcapi.PyBytesWriter(size, use_bytearray)
612+ with SuppressCrashReport():
613+ writer.write(0, data, check=False)
614+ try:
615+ { operation }
616+ except:
617+ # Ignore all exceptions
618+ pass
619+ # If we reached this line, the operation didn't
620+ # detect the overflow. Exit immediatetly without
621+ # calling the writer destructor since it can detect
622+ # the overflow.
623+ os._exit(0)
624+ """ )
625+ proc = assert_python_failure ('-c' , code )
626+ self .assertIn (b'Buffer overflow detected in PyBytesWriter' ,
627+ proc .err )
628+ self .assertIn (f'at position { size } ' .encode (),
629+ proc .err )
630+
631+ @unittest .skipUnless (support .Py_DEBUG , 'need debug build' )
632+ def test_get_data_canary (self ):
633+ # Test PyBytesWriter_GetData()
634+ NEW_BYTE = self .NEW_BYTE
635+ CANARY_BYTE = self .CANARY_BYTE
636+
637+ writer = self .create_writer (6 )
638+ self .assertEqual (get_data_canary (writer ),
639+ NEW_BYTE * 6 + CANARY_BYTE )
640+ writer .write (0 , b'abc' )
641+ self .assertEqual (get_data_canary (writer ),
642+ b'abc' + NEW_BYTE * 3 + CANARY_BYTE )
643+ writer .write (3 , b'123' )
644+ self .assertEqual (get_data_canary (writer ),
645+ b'abc123' + CANARY_BYTE )
646+
516647
517648class BytesWriterTest (BaseWriterTest , unittest .TestCase ):
518649 RESULT_TYPE = bytes
0 commit comments