Skip to content

Commit 8db671a

Browse files
Merge branch '3.14' into backport-5915a1f-3.14
2 parents e1dbafb + 8558396 commit 8db671a

30 files changed

Lines changed: 760 additions & 57 deletions

Doc/library/pyexpat.rst

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,71 @@ XMLParser Objects
238238
.. versionadded:: 3.13
239239

240240

241-
:class:`!xmlparser` objects have the following methods to mitigate some
242-
common XML vulnerabilities.
241+
:class:`!xmlparser` objects have the following methods to tune protections
242+
against some common XML vulnerabilities.
243+
244+
.. method:: xmlparser.SetBillionLaughsAttackProtectionActivationThreshold(threshold, /)
245+
246+
Sets the number of output bytes needed to activate protection against
247+
`billion laughs`_ attacks.
248+
249+
The number of output bytes includes amplification from entity expansion
250+
and reading DTD files.
251+
252+
Parser objects usually have a protection activation threshold of 8 MiB,
253+
but the actual default value depends on the underlying Expat library.
254+
255+
An :exc:`ExpatError` is raised if this method is called on a
256+
|xml-non-root-parser| parser.
257+
The corresponding :attr:`~ExpatError.lineno` and :attr:`~ExpatError.offset`
258+
should not be used as they may have no special meaning.
259+
260+
.. note::
261+
262+
Activation thresholds below 4 MiB are known to break support for DITA 1.3
263+
payload and are hence not recommended.
264+
265+
.. versionadded:: next
266+
267+
.. method:: xmlparser.SetBillionLaughsAttackProtectionMaximumAmplification(max_factor, /)
268+
269+
Sets the maximum tolerated amplification factor for protection against
270+
`billion laughs`_ attacks.
271+
272+
The amplification factor is calculated as ``(direct + indirect) / direct``
273+
while parsing, where ``direct`` is the number of bytes read from
274+
the primary document in parsing and ``indirect`` is the number of
275+
bytes added by expanding entities and reading of external DTD files.
276+
277+
The *max_factor* value must be a non-NaN :class:`float` value greater than
278+
or equal to 1.0. Peak amplifications of factor 15,000 for the entire payload
279+
and of factor 30,000 in the middle of parsing have been observed with small
280+
benign files in practice. In particular, the activation threshold should be
281+
carefully chosen to avoid false positives.
282+
283+
Parser objects usually have a maximum amplification factor of 100,
284+
but the actual default value depends on the underlying Expat library.
285+
286+
An :exc:`ExpatError` is raised if this method is called on a
287+
|xml-non-root-parser| parser or if *max_factor* is outside the valid range.
288+
The corresponding :attr:`~ExpatError.lineno` and :attr:`~ExpatError.offset`
289+
should not be used as they may have no special meaning.
290+
291+
.. note::
292+
293+
The maximum amplification factor is only considered if the threshold
294+
that can be adjusted by :meth:`.SetBillionLaughsAttackProtectionActivationThreshold`
295+
is exceeded.
296+
297+
.. versionadded:: next
243298

244299
.. method:: xmlparser.SetAllocTrackerActivationThreshold(threshold, /)
245300

246301
Sets the number of allocated bytes of dynamic memory needed to activate
247302
protection against disproportionate use of RAM.
248303

249-
By default, parser objects have an allocation activation threshold of 64 MiB,
250-
or equivalently 67,108,864 bytes.
304+
Parser objects usually have an allocation activation threshold of 64 MiB,
305+
but the actual default value depends on the underlying Expat library.
251306

252307
An :exc:`ExpatError` is raised if this method is called on a
253308
|xml-non-root-parser| parser.
@@ -271,7 +326,8 @@ common XML vulnerabilities.
271326
near the start of parsing even with benign files in practice. In particular,
272327
the activation threshold should be carefully chosen to avoid false positives.
273328

274-
By default, parser objects have a maximum amplification factor of 100.0.
329+
Parser objects usually have a maximum amplification factor of 100,
330+
but the actual default value depends on the underlying Expat library.
275331

276332
An :exc:`ExpatError` is raised if this method is called on a
277333
|xml-non-root-parser| parser or if *max_factor* is outside the valid range.
@@ -1019,4 +1075,6 @@ The ``errors`` module has the following attributes:
10191075
not. See https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-EncodingDecl
10201076
and https://www.iana.org/assignments/character-sets/character-sets.xhtml.
10211077
1078+
1079+
.. _billion laughs: https://en.wikipedia.org/wiki/Billion_laughs_attack
10221080
.. |xml-non-root-parser| replace:: :ref:`non-root <xmlparser-non-root>`

Doc/library/shutil.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,10 +388,14 @@ Directory and files operations
388388
If *dst* already exists but is not a directory, it may be overwritten
389389
depending on :func:`os.rename` semantics.
390390

391-
If the destination is on the current filesystem, then :func:`os.rename` is
392-
used. Otherwise, *src* is copied to the destination using *copy_function*
393-
and then removed. In case of symlinks, a new symlink pointing to the target
394-
of *src* will be created as the destination and *src* will be removed.
391+
:func:`os.rename` is preferably used internally when *src* and the destination are on
392+
the same filesystem. In case :func:`os.rename` fails due to :exc:`OSError`
393+
(e.g. the user has write permission to the destination file but not to its parent
394+
directory), this method falls back to using *copy_function*, in which case
395+
*src* is copied to the destination using *copy_function* and then removed.
396+
397+
In case of symlinks, a new symlink pointing to the target of *src* will be
398+
created in or as the destination, and *src* will be removed.
395399

396400
If *copy_function* is given, it must be a callable that takes two arguments,
397401
*src* and the destination, and will be used to copy *src* to the destination

Include/pyexpat.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ struct PyExpat_CAPI
5757
XML_Parser parser, unsigned long long activationThresholdBytes);
5858
XML_Bool (*SetAllocTrackerMaximumAmplification)(
5959
XML_Parser parser, float maxAmplificationFactor);
60+
/* might be NULL for expat < 2.4.0 */
61+
XML_Bool (*SetBillionLaughsAttackProtectionActivationThreshold)(
62+
XML_Parser parser, unsigned long long activationThresholdBytes);
63+
XML_Bool (*SetBillionLaughsAttackProtectionMaximumAmplification)(
64+
XML_Parser parser, float maxAmplificationFactor);
6065
/* always add new stuff to the end! */
6166
};
6267

Lib/shutil.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -885,10 +885,14 @@ def move(src, dst, copy_function=copy2):
885885
If dst already exists but is not a directory, it may be overwritten
886886
depending on os.rename() semantics.
887887
888-
If the destination is on our current filesystem, then rename() is used.
889-
Otherwise, src is copied to the destination and then removed. Symlinks are
890-
recreated under the new name if os.rename() fails because of cross
891-
filesystem renames.
888+
os.rename() is preferably used if the source and destination are on the
889+
same filesystem. In case os.rename() fails due to OSError (e.g. the user
890+
has write permission to *dst* file but not to its parent directory),
891+
this method falls back to using *copy_function* silently.
892+
Symlinks are also recreated under the new name if os.rename() fails
893+
because of cross filesystem renames.
894+
895+
It's recommended to use os.rename() if atomic move is strictly required.
892896
893897
The optional `copy_function` argument is a callable that will be used
894898
to copy the source or it will be delegated to `copytree`.

Lib/test/datetimetester.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7313,6 +7313,36 @@ def func():
73137313
self.assertEqual(out, b"a" * 8)
73147314
self.assertEqual(err, b"")
73157315

7316+
@support.cpython_only
7317+
@support.subTests(("setup", "call"), [
7318+
("obj = _datetime.timedelta", "obj(seconds=2)"),
7319+
("obj = _datetime.timedelta(seconds=2)", "obj.total_seconds()"),
7320+
("obj = _datetime.date(2026, 6, 7)", "obj.isocalendar()"),
7321+
])
7322+
def test_static_datetime_types_outlive_collected_module(self, setup, call):
7323+
# gh-151039: This code used to crash
7324+
script = f"""if True:
7325+
import sys, gc
7326+
import _datetime
7327+
7328+
{setup} # static C type, survives the module
7329+
del sys.modules['_datetime']
7330+
del _datetime
7331+
sys.modules['_datetime'] = None # block re-import
7332+
gc.collect() # module object is collected
7333+
7334+
try:
7335+
{call} # used to be a segmentation fault
7336+
except ImportError:
7337+
pass
7338+
else:
7339+
raise AssertionError("ImportError not raised")
7340+
"""
7341+
rc, out, err = script_helper.assert_python_ok("-c", script)
7342+
self.assertEqual(rc, 0)
7343+
self.assertEqual(out, b'')
7344+
self.assertEqual(err, b'')
7345+
73167346

73177347
def load_tests(loader, standard_tests, pattern):
73187348
standard_tests.addTest(ZoneInfoCompleteTest())

Lib/test/test_capi/test_weakref.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import weakref
2+
import unittest
3+
from test.support import import_helper
4+
5+
_testcapi = import_helper.import_module('_testcapi')
6+
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
7+
NULL = None
8+
9+
class Object:
10+
pass
11+
12+
class Ref(weakref.ReferenceType):
13+
pass
14+
15+
16+
class CAPIWeakrefTest(unittest.TestCase):
17+
def test_pyweakref_check(self):
18+
# Test PyWeakref_Check()
19+
check = _testlimitedcapi.pyweakref_check
20+
obj = Object()
21+
self.assertEqual(check(obj), 0)
22+
self.assertEqual(check(weakref.ref(obj)), 1)
23+
self.assertEqual(check(Ref(obj)), 1)
24+
self.assertEqual(check(weakref.proxy(obj)), 1)
25+
26+
# CRASHES check(NULL)
27+
28+
def test_pyweakref_checkref(self):
29+
# Test PyWeakref_CheckRef()
30+
checkref = _testlimitedcapi.pyweakref_checkref
31+
obj = Object()
32+
self.assertEqual(checkref(obj), 0)
33+
self.assertEqual(checkref(weakref.ref(obj)), 1)
34+
self.assertEqual(checkref(Ref(obj)), 1)
35+
self.assertEqual(checkref(weakref.proxy(obj)), 0)
36+
37+
# CRASHES checkref(NULL)
38+
39+
def test_pyweakref_checkrefexact(self):
40+
# Test PyWeakref_CheckRefExact()
41+
checkrefexact = _testlimitedcapi.pyweakref_checkrefexact
42+
obj = Object()
43+
self.assertEqual(checkrefexact(obj), 0)
44+
self.assertEqual(checkrefexact(weakref.ref(obj)), 1)
45+
self.assertEqual(checkrefexact(Ref(obj)), 0)
46+
self.assertEqual(checkrefexact(weakref.proxy(obj)), 0)
47+
48+
# CRASHES checkrefexact(NULL)
49+
50+
def test_pyweakref_checkproxy(self):
51+
# Test PyWeakref_CheckProxy()
52+
checkproxy = _testlimitedcapi.pyweakref_checkproxy
53+
obj = Object()
54+
self.assertEqual(checkproxy(obj), 0)
55+
self.assertEqual(checkproxy(weakref.ref(obj)), 0)
56+
self.assertEqual(checkproxy(Ref(obj)), 0)
57+
self.assertEqual(checkproxy(weakref.proxy(obj)), 1)
58+
59+
# CRASHES checkproxy(NULL)
60+
61+
def test_pyweakref_getref(self):
62+
# Test PyWeakref_GetRef()
63+
getref = _testcapi.pyweakref_getref
64+
obj = Object()
65+
wr = weakref.ref(obj)
66+
wp = weakref.proxy(obj)
67+
self.assertEqual(getref(wr), (1, obj))
68+
self.assertEqual(getref(wp), (1, obj))
69+
del obj
70+
self.assertEqual(getref(wr), 0)
71+
self.assertEqual(getref(wp), 0)
72+
73+
self.assertRaises(TypeError, getref, 42)
74+
self.assertRaises(SystemError, getref, NULL)
75+
76+
def test_pyweakref_isdead(self):
77+
# Test PyWeakref_IsDead()
78+
isdead = _testcapi.pyweakref_isdead
79+
obj = Object()
80+
wr = weakref.ref(obj)
81+
wp = weakref.proxy(obj)
82+
self.assertEqual(isdead(wr), 0)
83+
self.assertEqual(isdead(wp), 0)
84+
del obj
85+
self.assertEqual(isdead(wr), 1)
86+
self.assertEqual(isdead(wp), 1)
87+
88+
self.assertRaises(TypeError, isdead, 42)
89+
self.assertRaises(SystemError, isdead, NULL)
90+
91+
def test_pyweakref_newref(self):
92+
# Test PyWeakref_NewRef()
93+
newref = _testlimitedcapi.pyweakref_newref
94+
obj = Object()
95+
wr = newref(obj)
96+
self.assertIs(type(wr), weakref.ReferenceType)
97+
# PyWeakref_NewRef() handles None callback as NULL callback
98+
wr = newref(obj, None)
99+
self.assertIs(type(wr), weakref.ReferenceType)
100+
log = []
101+
wr = newref(obj, log.append)
102+
self.assertIs(type(wr), weakref.ReferenceType)
103+
self.assertEqual(log, [])
104+
del obj
105+
self.assertEqual(log, [wr])
106+
107+
self.assertRaises(TypeError, newref, [])
108+
# CRASHES newref(NULL)
109+
110+
def test_pyweakref_newproxy(self):
111+
# Test PyWeakref_NewProxy()
112+
newproxy = _testlimitedcapi.pyweakref_newproxy
113+
obj = Object()
114+
wp = newproxy(obj)
115+
self.assertIs(type(wp), weakref.ProxyType)
116+
# PyWeakref_NewProxy() handles None callback as NULL callback
117+
wp = newproxy(obj, None)
118+
self.assertIs(type(wp), weakref.ProxyType)
119+
log = []
120+
wp = newproxy(obj, log.append)
121+
self.assertIs(type(wp), weakref.ProxyType)
122+
self.assertEqual(log, [])
123+
del obj
124+
self.assertEqual(log, [wp])
125+
126+
def func():
127+
pass
128+
wp = newproxy(func)
129+
self.assertIs(type(wp), weakref.CallableProxyType)
130+
131+
self.assertRaises(TypeError, newproxy, [])
132+
# CRASHES newproxy(NULL)
133+
134+
135+
if __name__ == "__main__":
136+
unittest.main()

Lib/test/test_pyexpat.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,64 @@ def test_set_maximum_amplification__fail_for_subparser(self):
11831183
self.assert_root_parser_failure(setter, 123.45)
11841184

11851185

1186+
@unittest.skipIf(expat.version_info < (2, 4, 0), "requires Expat >= 2.4.0")
1187+
class ExpansionProtectionTest(AttackProtectionTestBase, unittest.TestCase):
1188+
1189+
def assert_rejected(self, func, /, *args, **kwargs):
1190+
"""Check that func(*args, **kwargs) hits the allocation limit."""
1191+
msg = (
1192+
r"limit on input amplification factor \(from DTD and entities\) "
1193+
r"breached: line \d+, column \d+"
1194+
)
1195+
self.assertRaisesRegex(expat.ExpatError, msg, func, *args, **kwargs)
1196+
1197+
def set_activation_threshold(self, parser, threshold):
1198+
return parser.SetBillionLaughsAttackProtectionActivationThreshold(threshold)
1199+
1200+
def set_maximum_amplification(self, parser, max_factor):
1201+
return parser.SetBillionLaughsAttackProtectionMaximumAmplification(max_factor)
1202+
1203+
def test_set_activation_threshold__threshold_reached(self):
1204+
parser = expat.ParserCreate()
1205+
# Choose a threshold expected to be always reached.
1206+
self.set_activation_threshold(parser, 3)
1207+
# Check that the threshold is reached by choosing a small factor
1208+
# and a payload whose peak amplification factor exceeds it.
1209+
self.assertIsNone(self.set_maximum_amplification(parser, 1.0))
1210+
payload = self.exponential_expansion_payload(ncols=10, nrows=4)
1211+
self.assert_rejected(parser.Parse, payload, True)
1212+
1213+
def test_set_activation_threshold__threshold_not_reached(self):
1214+
parser = expat.ParserCreate()
1215+
# Choose a threshold expected to be never reached.
1216+
self.set_activation_threshold(parser, pow(10, 5))
1217+
# Check that the threshold is reached by choosing a small factor
1218+
# and a payload whose peak amplification factor exceeds it.
1219+
self.assertIsNone(self.set_maximum_amplification(parser, 1.0))
1220+
payload = self.exponential_expansion_payload(ncols=10, nrows=4)
1221+
self.assertIsNotNone(parser.Parse(payload, True))
1222+
1223+
def test_set_maximum_amplification__amplification_exceeded(self):
1224+
parser = expat.ParserCreate()
1225+
# Unconditionally enable maximum activation factor.
1226+
self.set_activation_threshold(parser, 0)
1227+
# Choose a max amplification factor expected to always be exceeded.
1228+
self.assertIsNone(self.set_maximum_amplification(parser, 1.0))
1229+
# Craft a payload for which the peak amplification factor is > 1.0.
1230+
payload = self.exponential_expansion_payload(ncols=1, nrows=2)
1231+
self.assert_rejected(parser.Parse, payload, True)
1232+
1233+
def test_set_maximum_amplification__amplification_not_exceeded(self):
1234+
parser = expat.ParserCreate()
1235+
# Unconditionally enable maximum activation factor.
1236+
self.set_activation_threshold(parser, 0)
1237+
# Choose a max amplification factor expected to never be exceeded.
1238+
self.assertIsNone(self.set_maximum_amplification(parser, 1e4))
1239+
# Craft a payload for which the peak amplification factor is < 1e4.
1240+
payload = self.exponential_expansion_payload(ncols=1, nrows=2)
1241+
self.assertIsNotNone(parser.Parse(payload, True))
1242+
1243+
11861244
@unittest.skipIf(not hasattr(expat.XMLParserType,
11871245
"SetAllocTrackerMaximumAmplification"),
11881246
"requires Python compiled with Expat >= 2.7.2")

Mac/BuildScript/build-installer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,9 @@ def library_recipes():
378378
install=f"make && ranlib libsqlite3.a && make install DESTDIR={shellQuote(os.path.join(WORKDIR, 'libraries'))}",
379379
),
380380
dict(
381-
name="libmpdec 4.0.0",
382-
url="https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-4.0.0.tar.gz",
383-
checksum="942445c3245b22730fd41a67a7c5c231d11cb1b9936b9c0f76334fb7d0b4468c",
381+
name="libmpdec 4.0.1",
382+
url="https://www.bytereef.org/software/mpdecimal/releases/mpdecimal-4.0.1.tar.gz",
383+
checksum="96d33abb4bb0070c7be0fed4246cd38416188325f820468214471938545b1ac8",
384384
configure_pre=[
385385
"--disable-cxx",
386386
"MACHINE=universal",

0 commit comments

Comments
 (0)