Skip to content

Commit ee7397b

Browse files
authored
Merge branch '3.13' into backport-5a4c4a0-3.13
2 parents ebde876 + ed5d717 commit ee7397b

8 files changed

Lines changed: 171 additions & 36 deletions

File tree

Doc/c-api/descriptor.rst

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,46 @@ found in the dictionary of type objects.
2121
.. c:function:: PyObject* PyDescr_NewMember(PyTypeObject *type, struct PyMemberDef *meth)
2222
2323
24+
.. c:var:: PyTypeObject PyMemberDescr_Type
25+
26+
The type object for member descriptor objects created from
27+
:c:type:`PyMemberDef` structures. These descriptors expose fields of a
28+
C struct as attributes on a type, and correspond
29+
to :class:`types.MemberDescriptorType` objects in Python.
30+
31+
32+
33+
.. c:var:: PyTypeObject PyGetSetDescr_Type
34+
35+
The type object for get/set descriptor objects created from
36+
:c:type:`PyGetSetDef` structures. These descriptors implement attributes
37+
whose value is computed by C getter and setter functions, and are used
38+
for many built-in type attributes.
39+
40+
2441
.. c:function:: PyObject* PyDescr_NewMethod(PyTypeObject *type, struct PyMethodDef *meth)
2542
2643
44+
.. c:var:: PyTypeObject PyMethodDescr_Type
45+
46+
The type object for method descriptor objects created from
47+
:c:type:`PyMethodDef` structures. These descriptors expose C functions as
48+
methods on a type, and correspond to :class:`types.MemberDescriptorType`
49+
objects in Python.
50+
51+
2752
.. c:function:: PyObject* PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *wrapper, void *wrapped)
2853
2954
55+
.. c:var:: PyTypeObject PyWrapperDescr_Type
56+
57+
The type object for wrapper descriptor objects created by
58+
:c:func:`PyDescr_NewWrapper` and :c:func:`PyWrapper_New`. Wrapper
59+
descriptors are used internally to expose special methods implemented
60+
via wrapper structures, and appear in Python as
61+
:class:`types.WrapperDescriptorType` objects.
62+
63+
3064
.. c:function:: PyObject* PyDescr_NewClassMethod(PyTypeObject *type, PyMethodDef *method)
3165
3266
@@ -55,6 +89,14 @@ Built-in descriptors
5589
:class:`classmethod` in the Python layer.
5690
5791
92+
.. c:var:: PyTypeObject PyClassMethodDescr_Type
93+
94+
The type object for C-level class method descriptor objects.
95+
This is the type of the descriptors created for :func:`classmethod` defined in
96+
C extension types, and is the same object as :class:`classmethod`
97+
in Python.
98+
99+
58100
.. c:function:: PyObject *PyClassMethod_New(PyObject *callable)
59101
60102
Create a new :class:`classmethod` object wrapping *callable*.

Doc/c-api/dict.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ Dictionary Objects
4343
prevent modification of the dictionary for non-dynamic class types.
4444
4545
46+
.. c:var:: PyTypeObject PyDictProxy_Type
47+
48+
The type object for mapping proxy objects created by
49+
:c:func:`PyDictProxy_New` and for the read-only ``__dict__`` attribute
50+
of many built-in types. A :c:type:`PyDictProxy_Type` instance provides a
51+
dynamic, read-only view of an underlying dictionary: changes to the
52+
underlying dictionary are reflected in the proxy, but the proxy itself
53+
does not support mutation operations. This corresponds to
54+
:class:`types.MappingProxyType` in Python.
55+
56+
4657
.. c:function:: void PyDict_Clear(PyObject *p)
4758
4859
Empty an existing dictionary of all key-value pairs.

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,12 +208,8 @@ def _write_atomic(path, data, mode=0o666):
208208
try:
209209
# We first write data to a temporary file, and then use os.replace() to
210210
# perform an atomic rename.
211-
with _io.FileIO(fd, 'wb') as file:
212-
bytes_written = file.write(data)
213-
if bytes_written != len(data):
214-
# Raise an OSError so the 'except' below cleans up the partially
215-
# written file.
216-
raise OSError("os.write() didn't write the full pyc file")
211+
with _io.open(fd, 'wb') as file:
212+
file.write(data)
217213
_os.replace(path_tmp, path)
218214
except OSError:
219215
try:

Lib/plistlib.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@
7373
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
7474
globals().update(PlistFormat.__members__)
7575

76+
# Data larger than this will be read in chunks, to prevent extreme
77+
# overallocation.
78+
_MIN_READ_BUF_SIZE = 1 << 20
7679

7780
class UID:
7881
def __init__(self, data):
@@ -508,12 +511,24 @@ def _get_size(self, tokenL):
508511

509512
return tokenL
510513

514+
def _read(self, size):
515+
cursize = min(size, _MIN_READ_BUF_SIZE)
516+
data = self._fp.read(cursize)
517+
while True:
518+
if len(data) != cursize:
519+
raise InvalidFileException
520+
if cursize == size:
521+
return data
522+
delta = min(cursize, size - cursize)
523+
data += self._fp.read(delta)
524+
cursize += delta
525+
511526
def _read_ints(self, n, size):
512-
data = self._fp.read(size * n)
527+
data = self._read(size * n)
513528
if size in _BINARY_FORMAT:
514529
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
515530
else:
516-
if not size or len(data) != size * n:
531+
if not size:
517532
raise InvalidFileException()
518533
return tuple(int.from_bytes(data[i: i + size], 'big')
519534
for i in range(0, size * n, size))
@@ -573,22 +588,16 @@ def _read_object(self, ref):
573588

574589
elif tokenH == 0x40: # data
575590
s = self._get_size(tokenL)
576-
result = self._fp.read(s)
577-
if len(result) != s:
578-
raise InvalidFileException()
591+
result = self._read(s)
579592

580593
elif tokenH == 0x50: # ascii string
581594
s = self._get_size(tokenL)
582-
data = self._fp.read(s)
583-
if len(data) != s:
584-
raise InvalidFileException()
595+
data = self._read(s)
585596
result = data.decode('ascii')
586597

587598
elif tokenH == 0x60: # unicode string
588599
s = self._get_size(tokenL) * 2
589-
data = self._fp.read(s)
590-
if len(data) != s:
591-
raise InvalidFileException()
600+
data = self._read(s)
592601
result = data.decode('utf-16be')
593602

594603
elif tokenH == 0x80: # UID

Lib/test/test_importlib/test_util.py

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -789,31 +789,70 @@ def test_complete_multi_phase_init_module(self):
789789
self.run_with_own_gil(script)
790790

791791

792-
class MiscTests(unittest.TestCase):
793-
def test_atomic_write_should_notice_incomplete_writes(self):
792+
class PatchAtomicWrites:
793+
def __init__(self, truncate_at_length, never_complete=False):
794+
self.truncate_at_length = truncate_at_length
795+
self.never_complete = never_complete
796+
self.seen_write = False
797+
self._children = []
798+
799+
def __enter__(self):
794800
import _pyio
795801

796802
oldwrite = os.write
797-
seen_write = False
798-
799-
truncate_at_length = 100
800803

801804
# Emulate an os.write that only writes partial data.
802805
def write(fd, data):
803-
nonlocal seen_write
804-
seen_write = True
805-
return oldwrite(fd, data[:truncate_at_length])
806+
if self.seen_write and self.never_complete:
807+
return None
808+
self.seen_write = True
809+
return oldwrite(fd, data[:self.truncate_at_length])
806810

807811
# Need to patch _io to be _pyio, so that io.FileIO is affected by the
808812
# os.write patch.
809-
with (support.swap_attr(_bootstrap_external, '_io', _pyio),
810-
support.swap_attr(os, 'write', write)):
811-
with self.assertRaises(OSError):
812-
# Make sure we write something longer than the point where we
813-
# truncate.
814-
content = b'x' * (truncate_at_length * 2)
815-
_bootstrap_external._write_atomic(os_helper.TESTFN, content)
816-
assert seen_write
813+
self.children = [
814+
support.swap_attr(_bootstrap_external, '_io', _pyio),
815+
support.swap_attr(os, 'write', write)
816+
]
817+
for child in self.children:
818+
child.__enter__()
819+
return self
820+
821+
def __exit__(self, exc_type, exc_val, exc_tb):
822+
for child in self.children:
823+
child.__exit__(exc_type, exc_val, exc_tb)
824+
825+
826+
class MiscTests(unittest.TestCase):
827+
828+
def test_atomic_write_retries_incomplete_writes(self):
829+
truncate_at_length = 100
830+
length = truncate_at_length * 2
831+
832+
with PatchAtomicWrites(truncate_at_length=truncate_at_length) as cm:
833+
# Make sure we write something longer than the point where we
834+
# truncate.
835+
content = b'x' * length
836+
_bootstrap_external._write_atomic(os_helper.TESTFN, content)
837+
self.assertTrue(cm.seen_write)
838+
839+
self.assertEqual(os.stat(support.os_helper.TESTFN).st_size, length)
840+
os.unlink(support.os_helper.TESTFN)
841+
842+
def test_atomic_write_errors_if_unable_to_complete(self):
843+
truncate_at_length = 100
844+
845+
with (
846+
PatchAtomicWrites(
847+
truncate_at_length=truncate_at_length, never_complete=True,
848+
) as cm,
849+
self.assertRaises(OSError)
850+
):
851+
# Make sure we write something longer than the point where we
852+
# truncate.
853+
content = b'x' * (truncate_at_length * 2)
854+
_bootstrap_external._write_atomic(os_helper.TESTFN, content)
855+
self.assertTrue(cm.seen_write)
817856

818857
with self.assertRaises(OSError):
819858
os.stat(support.os_helper.TESTFN) # Check that the file did not get written.

Lib/test/test_plistlib.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -904,8 +904,7 @@ def test_dump_naive_datetime_with_aware_datetime_option(self):
904904

905905
class TestBinaryPlistlib(unittest.TestCase):
906906

907-
@staticmethod
908-
def decode(*objects, offset_size=1, ref_size=1):
907+
def build(self, *objects, offset_size=1, ref_size=1):
909908
data = [b'bplist00']
910909
offset = 8
911910
offsets = []
@@ -917,7 +916,11 @@ def decode(*objects, offset_size=1, ref_size=1):
917916
len(objects), 0, offset)
918917
data.extend(offsets)
919918
data.append(tail)
920-
return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
919+
return b''.join(data)
920+
921+
def decode(self, *objects, offset_size=1, ref_size=1):
922+
data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
923+
return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
921924

922925
def test_nonstandard_refs_size(self):
923926
# Issue #21538: Refs and offsets are 24-bit integers
@@ -1025,6 +1028,34 @@ def test_invalid_binary(self):
10251028
with self.assertRaises(plistlib.InvalidFileException):
10261029
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
10271030

1031+
def test_truncated_large_data(self):
1032+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
1033+
def check(data):
1034+
with open(os_helper.TESTFN, 'wb') as f:
1035+
f.write(data)
1036+
# buffered file
1037+
with open(os_helper.TESTFN, 'rb') as f:
1038+
with self.assertRaises(plistlib.InvalidFileException):
1039+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
1040+
# unbuffered file
1041+
with open(os_helper.TESTFN, 'rb', buffering=0) as f:
1042+
with self.assertRaises(plistlib.InvalidFileException):
1043+
plistlib.load(f, fmt=plistlib.FMT_BINARY)
1044+
for w in range(20, 64):
1045+
s = 1 << w
1046+
# data
1047+
check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big')))
1048+
# ascii string
1049+
check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big')))
1050+
# unicode string
1051+
check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big')))
1052+
# array
1053+
check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big')))
1054+
# dict
1055+
check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big')))
1056+
# number of objects
1057+
check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8))
1058+
10281059
def test_load_aware_datetime(self):
10291060
data = (b'bplist003B\x04>\xd0d\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00'
10301061
b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When importing a module, use Python's regular file object to ensure that
2+
writes to ``.pyc`` files are complete or an appropriate error is raised.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a potential memory denial of service in the :mod:`plistlib` module.
2+
When reading a Plist file received from untrusted source, it could cause
3+
an arbitrary amount of memory to be allocated.
4+
This could have led to symptoms including a :exc:`MemoryError`, swapping, out
5+
of memory (OOM) killed processes or containers, or even system crashes.

0 commit comments

Comments
 (0)