Skip to content

Commit 556407d

Browse files
committed
Merge branch 'main' into is_immortal
2 parents 3ac1ba1 + 658612a commit 556407d

37 files changed

Lines changed: 713 additions & 147 deletions

Doc/c-api/bytes.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ called with a non-bytes parameter.
231231
Resize a bytes object. *newsize* will be the new length of the bytes object.
232232
You can think of it as creating a new bytes object and destroying the old
233233
one, only more efficiently.
234+
234235
Pass the address of an
235236
existing bytes object as an lvalue (it may be written into), and the new size
236237
desired. On success, *\*bytes* holds the resized bytes object and ``0`` is
@@ -239,6 +240,11 @@ called with a non-bytes parameter.
239240
*\*bytes* is set to ``NULL``, :exc:`MemoryError` is set, and ``-1`` is
240241
returned.
241242
243+
While bytes objects are usually immutable in Python, this special C API
244+
allows mutating a bytes object in-place. The returned bytes object can still
245+
be mutated using :c:func:`PyBytesWriter_GetData`; except if *newsize* is
246+
zero in which case it returns the immutable empty bytes string.
247+
242248
.. soft-deprecated:: 3.15
243249
Use the :c:type:`PyBytesWriter` API instead.
244250
@@ -290,10 +296,10 @@ object.
290296
291297
.. c:type:: PyBytesWriter
292298
293-
A bytes writer instance.
299+
A bytes writer object.
294300
295-
The API is **not thread safe**: a writer should only be used by a single
296-
thread at the same time.
301+
The API is **not thread safe**. A :c:type:`PyBytesWriter` object must only
302+
be used by a single thread, it must not be shared between threads.
297303
298304
The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
299305
success, or :c:func:`PyBytesWriter_Discard` on error.
@@ -429,7 +435,7 @@ Low-level API
429435
On success, return ``0``.
430436
On error, set an exception and return ``-1``.
431437
432-
*size* can be negative to shrink the writer.
438+
*grow* can be negative to shrink the writer.
433439
434440
.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
435441

Doc/library/json.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ Basic Usage
261261
into JSON and then back into a dictionary, the dictionary may not equal
262262
the original one. That is, ``loads(dumps(x)) != x`` if x has non-string
263263
keys.
264+
*sort_keys* sorts the keys before they are coerced to strings,
265+
so numeric keys are sorted by value, not by their string representation.
264266

265267
.. function:: load(fp, *, cls=None, object_hook=None, parse_float=None, \
266268
parse_int=None, parse_constant=None, \

Doc/library/tk.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ alternative `GUI frameworks and tools <https://wiki.python.org/moin/GuiProgrammi
3434
tkinter.colorchooser.rst
3535
tkinter.font.rst
3636
tkinter.fontchooser.rst
37-
dialog.rst
37+
tkinter.dialogs.rst
3838
tkinter.messagebox.rst
3939
tkinter.scrolledtext.rst
4040
tkinter.systray.rst
File renamed without changes.

Doc/tools/removed-ids.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,6 @@ reference/expressions.html: generator.__next__
8282
reference/expressions.html: generator.close
8383
reference/expressions.html: generator.send
8484
reference/expressions.html: generator.throw
85+
86+
# Renamed to library/tkinter.dialogs.html
87+
library/dialog.html: (page missing)

Include/internal/pycore_bytesobject.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ PyAPI_FUNC(PyObject *) _PyBytes_Repeat(PyObject *self, Py_ssize_t n);
7777

7878
extern int _PyBytes_ResizeKeepOnError(PyObject **pv, Py_ssize_t newsize);
7979

80+
#ifndef NDEBUG
81+
extern int _PyBytes_IsMutable(PyObject *obj);
82+
#endif
83+
8084
/* --- PyBytesWriter ------------------------------------------------------ */
8185

8286
struct PyBytesWriter {

Lib/idlelib/editor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,9 @@ def set_width(self):
325325
# http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21
326326
zero_char_width = \
327327
Font(text, font=text.cget('font')).measure('0')
328-
self.width = pixel_width // zero_char_width
328+
# Some fonts report a zero width for '0' (gh-90304).
329+
self.width = (pixel_width // zero_char_width if zero_char_width
330+
else text.tk.getint(text.cget('width')))
329331

330332
def new_callback(self, event):
331333
dirname, basename = self.io.defaultfilename()

Lib/idlelib/idle_test/test_editor.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from idlelib import editor
44
import unittest
55
from collections import namedtuple
6+
from unittest import mock
67
from test.support import requires
78
from tkinter import Tk, Text
89

@@ -30,6 +31,18 @@ def test_init(self):
3031
self.assertEqual(e.root, self.root)
3132
e._close()
3233

34+
def test_set_width_zero_char_width(self):
35+
# A zero-width '0' must not raise ZeroDivisionError (gh-90304).
36+
e = Editor(root=self.root)
37+
try:
38+
with mock.patch.object(editor, 'Font') as MockFont:
39+
MockFont.return_value.measure.return_value = 0
40+
e.set_width()
41+
self.assertEqual(e.width,
42+
e.text.tk.getint(e.text.cget('width')))
43+
finally:
44+
e._close()
45+
3346

3447
class GetLineIndentTest(unittest.TestCase):
3548
def test_empty_lines(self):

Lib/test/test_annotationlib.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1869,7 +1869,7 @@ def nested():
18691869
self.assertEqual(type_repr(t'''{ 0
18701870
& 1
18711871
| 2
1872-
}'''), 't"""{ 0\n & 1\n | 2}"""')
1872+
}'''), 't"""{ 0\n & 1\n | 2\n }"""')
18731873
self.assertEqual(
18741874
type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'"
18751875
)

Lib/test/test_capi/test_bytes.py

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import sys
2+
import textwrap
23
import unittest
34
from test import support
45
from 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+
319326
class 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

517648
class BytesWriterTest(BaseWriterTest, unittest.TestCase):
518649
RESULT_TYPE = bytes

0 commit comments

Comments
 (0)