Skip to content

Commit 1f1e8b1

Browse files
authored
Merge branch 'main' into gh-144986-fix-atexit-memleak
2 parents 7e3dd83 + e84a2cc commit 1f1e8b1

22 files changed

Lines changed: 209 additions & 59 deletions

Include/internal/pycore_critical_section.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ extern "C" {
5050
// Asserts that the mutex for the given object is locked. The mutex must
5151
// be held by the top-most critical section otherwise there's the
5252
// possibility that the mutex would be swalled out in some code paths.
53+
//
54+
// NOTE: We use Py_REFCNT(op) != 1 instead of
55+
// !PyUnstable_Object_IsUniquelyReferenced(op) because the free threading
56+
// GC can change an object's ob_tid (it overwrites ob_tid and later
57+
// restores it from the mimalloc segment). This means
58+
// PyUnstable_Object_IsUniquelyReferenced() may spuriously return false
59+
// after a GC collection, even though the thread may still have exclusive
60+
// access to the object. The refcount check is a looser but still catches
61+
// most misuses.
5362
#ifdef Py_DEBUG
5463

5564
# define _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op) \

Include/internal/pycore_interp_structs.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ struct _py_func_state {
496496

497497
/* For now we hard-code this to a value for which we are confident
498498
all the static builtin types will fit (for all builds). */
499-
#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES 200
499+
#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES 201
500500
#define _Py_MAX_MANAGED_STATIC_EXT_TYPES 10
501501
#define _Py_MAX_MANAGED_STATIC_TYPES \
502502
(_Py_MAX_MANAGED_STATIC_BUILTIN_TYPES + _Py_MAX_MANAGED_STATIC_EXT_TYPES)

InternalDocs/garbage_collector.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ pointer-sized (that is, eight bytes on a 64-bit platform).
153153

154154
The garbage collector also temporarily repurposes the `ob_tid` (thread ID)
155155
and `ob_ref_local` (local reference count) fields for other purposes during
156-
collections.
156+
collections. The `ob_tid` field is later restored from the containing
157+
mimalloc segment data structure. In some cases, such as when the original
158+
allocating thread exits, this can result in a different `ob_tid` value.
159+
Code should not rely on `ob_tid` being stable across operations that may
160+
trigger garbage collection.
157161

158162

159163
C APIs

Lib/dataclasses.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ class _KW_ONLY_TYPE:
191191
KW_ONLY = _KW_ONLY_TYPE()
192192

193193
# Since most per-field metadata will be unused, create an empty
194-
# read-only proxy that can be shared among all fields.
195-
_EMPTY_METADATA = types.MappingProxyType({})
194+
# read-only dictionary that can be shared among all fields.
195+
_EMPTY_METADATA = frozendict()
196196

197197
# Markers for the various kinds of fields and pseudo-fields.
198198
class _FIELD_BASE:

Lib/email/headerregistry.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
This module provides an implementation of the HeaderRegistry API.
44
The implementation is designed to flexibly follow RFC5322 rules.
55
"""
6-
from types import MappingProxyType
7-
86
from email import utils
97
from email import errors
108
from email import _header_value_parser as parser
@@ -462,7 +460,7 @@ def init(self, *args, **kw):
462460

463461
@property
464462
def params(self):
465-
return MappingProxyType(self._params)
463+
return frozendict(self._params)
466464

467465

468466
class ContentTypeHeader(ParameterizedMIMEHeader):

Lib/test/pickletester.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,29 @@ def __hash__(self):
14841484
# bad hashable dict key
14851485
self.check_unpickling_error(CustomError, base + b'}c__main__\nBadKey1\n)\x81Nsb.')
14861486

1487+
def test_bad_types(self):
1488+
# APPEND
1489+
self.assertEqual(self.loads(b']Na.'), [None])
1490+
self.check_unpickling_error(AttributeError, b'NNa.') # non-list
1491+
# APPENDS
1492+
self.assertEqual(self.loads(b'](Ne.'), [None])
1493+
self.check_unpickling_error(AttributeError, b'N(Ne.') # non-list
1494+
self.check_unpickling_error(AttributeError, b'N(e.')
1495+
# SETITEM
1496+
self.assertEqual(self.loads(b'}NNs.'), {None: None})
1497+
self.check_unpickling_error(TypeError, b'NNNs.') # non-dict
1498+
self.check_unpickling_error(TypeError, b'}]Ns.') # non-hashable key
1499+
# SETITEMS
1500+
self.assertEqual(self.loads(b'}(NNu.'), {None: None})
1501+
self.check_unpickling_error(TypeError, b'N(NNu.') # non-dict
1502+
self.assertEqual(self.loads(b'N(u.'), None) # no validation for empty items
1503+
self.check_unpickling_error(TypeError, b'}(]Nu.') # non-hashable key
1504+
# ADDITEMS
1505+
self.assertEqual(self.loads(b'\x8f(N\x90.'), {None})
1506+
self.check_unpickling_error(AttributeError, b'N(N\x90.') # non-set
1507+
self.check_unpickling_error(AttributeError, b'N(\x90.')
1508+
self.check_unpickling_error(TypeError, b'\x8f(]\x90.') # non-hashable element
1509+
14871510
def test_bad_stack(self):
14881511
badpickles = [
14891512
b'.', # STOP

Lib/test/support/hashlib_helper.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import unittest
77
import unittest.mock
88
from test.support import import_helper
9-
from types import MappingProxyType
109

1110

1211
def _parse_fullname(fullname, *, strict=False):
@@ -351,7 +350,7 @@ def __init__(
351350
)
352351

353352

354-
_HASHINFO_DATABASE = MappingProxyType({
353+
_HASHINFO_DATABASE = frozendict({
355354
_HashId.md5: _HashInfo(
356355
_HashId.md5,
357356
"_md5.MD5Type",
@@ -500,7 +499,7 @@ def _iter_hash_func_info(excluded):
500499
# keyed hash function. However, as it's exposed by HACL*, we test it.
501500
_HMACINFO_DATABASE[_HashId.blake2s] = _HashInfoItem('_hmac.compute_blake2s_32')
502501
_HMACINFO_DATABASE[_HashId.blake2b] = _HashInfoItem('_hmac.compute_blake2b_32')
503-
_HMACINFO_DATABASE = MappingProxyType(_HMACINFO_DATABASE)
502+
_HMACINFO_DATABASE = frozendict(_HMACINFO_DATABASE)
504503
assert _HMACINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES
505504

506505

Lib/test/test_dataclasses/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_field_repr(self):
7171
expected_output = "Field(name='id',type=None," \
7272
f"default=1,default_factory={MISSING!r}," \
7373
"init=True,repr=False,hash=None," \
74-
"compare=True,metadata=mappingproxy({})," \
74+
"compare=True,metadata=frozendict()," \
7575
f"kw_only={MISSING!r}," \
7676
"doc='Docstring'," \
7777
"_field_type=None)"

Lib/test/test_dict.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1787,6 +1787,44 @@ def test_hash(self):
17871787
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
17881788
hash(fd)
17891789

1790+
def test_fromkeys(self):
1791+
self.assertEqual(frozendict.fromkeys('abc'),
1792+
frozendict(a=None, b=None, c=None))
1793+
1794+
# Subclass which overrides the constructor
1795+
created = frozendict(x=1)
1796+
class FrozenDictSubclass(frozendict):
1797+
def __new__(self):
1798+
return created
1799+
1800+
fd = FrozenDictSubclass.fromkeys("abc")
1801+
self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None))
1802+
self.assertEqual(type(fd), FrozenDictSubclass)
1803+
self.assertEqual(created, frozendict(x=1))
1804+
1805+
fd = FrozenDictSubclass.fromkeys(frozendict(y=2))
1806+
self.assertEqual(fd, frozendict(x=1, y=None))
1807+
self.assertEqual(type(fd), FrozenDictSubclass)
1808+
self.assertEqual(created, frozendict(x=1))
1809+
1810+
# Subclass which doesn't override the constructor
1811+
class FrozenDictSubclass2(frozendict):
1812+
pass
1813+
1814+
fd = FrozenDictSubclass2.fromkeys("abc")
1815+
self.assertEqual(fd, frozendict(a=None, b=None, c=None))
1816+
self.assertEqual(type(fd), FrozenDictSubclass2)
1817+
1818+
# Dict subclass which overrides the constructor
1819+
class DictSubclass(dict):
1820+
def __new__(self):
1821+
return created
1822+
1823+
fd = DictSubclass.fromkeys("abc")
1824+
self.assertEqual(fd, frozendict(x=1, a=None, b=None, c=None))
1825+
self.assertEqual(type(fd), DictSubclass)
1826+
self.assertEqual(created, frozendict(x=1))
1827+
17901828

17911829
if __name__ == "__main__":
17921830
unittest.main()

Lib/test/test_hmac.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import hmac
2222
import hashlib
2323
import random
24-
import types
2524
import unittest
2625
import warnings
2726
from _operator import _compare_digest as operator_compare_digest
@@ -303,7 +302,7 @@ def assert_hmac_new_by_name(
303302

304303
def check_hmac_new(
305304
self, key, msg, hexdigest, hashname, digest_size, block_size,
306-
hmac_new_func, hmac_new_kwds=types.MappingProxyType({}),
305+
hmac_new_func, hmac_new_kwds=frozendict(),
307306
):
308307
"""Check that HMAC(key, msg) == digest.
309308
@@ -349,7 +348,7 @@ def assert_hmac_hexdigest_by_name(
349348

350349
def check_hmac_hexdigest(
351350
self, key, msg, hexdigest, digest_size,
352-
hmac_digest_func, hmac_digest_kwds=types.MappingProxyType({}),
351+
hmac_digest_func, hmac_digest_kwds=frozendict(),
353352
):
354353
"""Check and return a HMAC digest computed by hmac_digest_func().
355354

0 commit comments

Comments
 (0)