Skip to content

Commit a113124

Browse files
authored
[3.11] gh-89811: Check for valid tp_version_tag in specializer (GH-115045)
* gh-89811: Check for valid tp_version_tag in specializer (GH-113558) * gh-113937 Fix failures in type cache tests due to re-running (GH-113953) * Update backported code for 3.11 specifically
1 parent 2e99ba9 commit a113124

File tree

4 files changed

+216
-1
lines changed

4 files changed

+216
-1
lines changed

Lib/test/test_type_cache.py

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
""" Tests for the internal type cache in CPython. """
22
import unittest
3+
import dis
34
from test import support
45
from test.support import import_helper
56
try:
@@ -8,7 +9,17 @@
89
_clear_type_cache = None
910

1011
# Skip this test if the _testcapi module isn't available.
11-
type_get_version = import_helper.import_module('_testcapi').type_get_version
12+
_testcapi = import_helper.import_module("_testcapi")
13+
type_get_version = _testcapi.type_get_version
14+
type_assign_specific_version_unsafe = _testcapi.type_assign_specific_version_unsafe
15+
type_modified = _testcapi.type_modified
16+
17+
18+
def type_assign_version(type_):
19+
try:
20+
type_.x
21+
except AttributeError:
22+
pass
1223

1324

1425
@support.cpython_only
@@ -42,6 +53,151 @@ def test_tp_version_tag_unique(self):
4253
self.assertEqual(len(set(all_version_tags)), 30,
4354
msg=f"{all_version_tags} contains non-unique versions")
4455

56+
def test_type_assign_specific_version(self):
57+
"""meta-test for type_assign_specific_version_unsafe"""
58+
class C:
59+
pass
60+
61+
type_assign_version(C)
62+
orig_version = type_get_version(C)
63+
if orig_version == 0:
64+
self.skipTest("Could not assign a valid type version")
65+
66+
type_modified(C)
67+
type_assign_specific_version_unsafe(C, orig_version + 5)
68+
type_assign_version(C) # this should do nothing
69+
70+
new_version = type_get_version(C)
71+
self.assertEqual(new_version, orig_version + 5)
72+
73+
_clear_type_cache()
74+
75+
76+
@support.cpython_only
77+
class TypeCacheWithSpecializationTests(unittest.TestCase):
78+
def tearDown(self):
79+
_clear_type_cache()
80+
81+
def _assign_valid_version_or_skip(self, type_):
82+
type_modified(type_)
83+
type_assign_version(type_)
84+
if type_get_version(type_) == 0:
85+
self.skipTest("Could not assign valid type version")
86+
87+
def _assign_and_check_version_0(self, user_type):
88+
type_modified(user_type)
89+
type_assign_specific_version_unsafe(user_type, 0)
90+
self.assertEqual(type_get_version(user_type), 0)
91+
92+
def _all_opnames(self, func):
93+
return set(instr.opname for instr in dis.Bytecode(func, adaptive=True))
94+
95+
def _check_specialization(self, func, arg, opname, *, should_specialize):
96+
for _ in range(100):
97+
func(arg)
98+
99+
if should_specialize:
100+
self.assertNotIn(opname, self._all_opnames(func))
101+
else:
102+
self.assertIn(opname, self._all_opnames(func))
103+
104+
def test_load_method_specialization_user_type(self):
105+
class A:
106+
def foo(self):
107+
pass
108+
109+
self._assign_valid_version_or_skip(A)
110+
111+
def load_foo_1(instance):
112+
instance.foo()
113+
114+
self._check_specialization(
115+
load_foo_1, A(), "LOAD_METHOD_ADAPTIVE", should_specialize=True
116+
)
117+
del load_foo_1
118+
119+
self._assign_and_check_version_0(A)
120+
121+
def load_foo_2(instance):
122+
instance.foo()
123+
124+
self._check_specialization(
125+
load_foo_2, A(), "LOAD_METHOD_ADAPTIVE", should_specialize=False
126+
)
127+
128+
def test_store_attr_specialization_user_type(self):
129+
class B:
130+
__slots__ = ("bar",)
131+
132+
self._assign_valid_version_or_skip(B)
133+
134+
def store_bar_1(instance):
135+
instance.bar = 10
136+
137+
self._check_specialization(
138+
store_bar_1, B(), "STORE_ATTR_ADAPTIVE", should_specialize=True
139+
)
140+
del store_bar_1
141+
142+
self._assign_and_check_version_0(B)
143+
144+
def store_bar_2(instance):
145+
instance.bar = 10
146+
147+
self._check_specialization(
148+
store_bar_2, B(), "STORE_ATTR_ADAPTIVE", should_specialize=False
149+
)
150+
151+
def test_load_attr_specialization_user_type(self):
152+
class C:
153+
__slots__ = ("biz",)
154+
def __init__(self):
155+
self.biz = 8
156+
157+
self._assign_valid_version_or_skip(C)
158+
159+
def load_biz_1(type_):
160+
type_.biz
161+
162+
self._check_specialization(
163+
load_biz_1, C(), "LOAD_ATTR_ADAPTIVE", should_specialize=True
164+
)
165+
del load_biz_1
166+
167+
self._assign_and_check_version_0(C)
168+
169+
def load_biz_2(type_):
170+
type_.biz
171+
172+
self._check_specialization(
173+
load_biz_2, C(), "LOAD_ATTR_ADAPTIVE", should_specialize=False
174+
)
175+
176+
def test_binary_subscript_specialization_user_type(self):
177+
class D:
178+
def __getitem__(self, _):
179+
return 1
180+
181+
self._assign_valid_version_or_skip(D)
182+
183+
def subscript_1(instance):
184+
instance[6]
185+
186+
self._check_specialization(
187+
subscript_1, D(), "BINARY_SUBSCR_ADAPTIVE", should_specialize=True
188+
)
189+
del subscript_1
190+
191+
self._assign_and_check_version_0(D)
192+
193+
def subscript_2(instance):
194+
instance[6]
195+
196+
self._check_specialization(
197+
subscript_2, D(), "BINARY_SUBSCR_ADAPTIVE", should_specialize=False
198+
)
199+
200+
45201

46202
if __name__ == "__main__":
47203
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Check for a valid ``tp_version_tag`` before performing bytecode specializations that
2+
rely on this value being usable.

Modules/_testcapimodule.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5899,6 +5899,32 @@ type_get_version(PyObject *self, PyObject *type)
58995899
return res;
59005900
}
59015901

5902+
static PyObject *
5903+
type_modified(PyObject *self, PyObject *type)
5904+
{
5905+
if (!PyType_Check(type)) {
5906+
PyErr_SetString(PyExc_TypeError, "argument must be a type");
5907+
return NULL;
5908+
}
5909+
PyType_Modified((PyTypeObject *)type);
5910+
Py_RETURN_NONE;
5911+
}
5912+
5913+
// Circumvents standard version assignment machinery - use with caution and only on
5914+
// short-lived heap types
5915+
static PyObject *
5916+
type_assign_specific_version_unsafe(PyObject *self, PyObject *args)
5917+
{
5918+
PyTypeObject *type;
5919+
unsigned int version;
5920+
if (!PyArg_ParseTuple(args, "Oi:type_assign_specific_version_unsafe", &type, &version)) {
5921+
return NULL;
5922+
}
5923+
assert(!PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE));
5924+
type->tp_version_tag = version;
5925+
type->tp_flags |= Py_TPFLAGS_VALID_VERSION_TAG;
5926+
Py_RETURN_NONE;
5927+
}
59025928

59035929
// Test PyThreadState C API
59045930
static PyObject *
@@ -6782,6 +6808,9 @@ static PyMethodDef TestMethods[] = {
67826808
{"fatal_error", test_fatal_error, METH_VARARGS,
67836809
PyDoc_STR("fatal_error(message, release_gil=False): call Py_FatalError(message)")},
67846810
{"type_get_version", type_get_version, METH_O, PyDoc_STR("type->tp_version_tag")},
6811+
{"type_modified", type_modified, METH_O, PyDoc_STR("PyType_Modified")},
6812+
{"type_assign_specific_version_unsafe", type_assign_specific_version_unsafe, METH_VARARGS,
6813+
PyDoc_STR("forcefully assign type->tp_version_tag")},
67856814
{"test_tstate_capi", test_tstate_capi, METH_NOARGS, NULL},
67866815
{"float_pack", test_float_pack, METH_VARARGS, NULL},
67876816
{"float_unpack", test_float_unpack, METH_VARARGS, NULL},

Python/specialize.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ miss_counter_start(void) {
481481
#define SPEC_FAIL_UNPACK_SEQUENCE_ITERATOR 8
482482
#define SPEC_FAIL_UNPACK_SEQUENCE_SEQUENCE 9
483483

484+
static uint32_t type_get_version(PyTypeObject *t, int opcode);
484485

485486
static int
486487
specialize_module_load_attr(PyObject *owner, _Py_CODEUNIT *instr,
@@ -673,6 +674,9 @@ _Py_Specialize_LoadAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
673674
}
674675
PyObject *descr;
675676
DescriptorClassification kind = analyze_descriptor(type, name, &descr, 0);
677+
if (type_get_version(type, LOAD_ATTR) == 0) {
678+
goto fail;
679+
}
676680
switch(kind) {
677681
case OVERRIDING:
678682
SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_ATTR_OVERRIDING_DESCRIPTOR);
@@ -766,6 +770,9 @@ _Py_Specialize_StoreAttr(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
766770
}
767771
PyObject *descr;
768772
DescriptorClassification kind = analyze_descriptor(type, name, &descr, 1);
773+
if (type_get_version(type, STORE_ATTR) == 0) {
774+
goto fail;
775+
}
769776
switch(kind) {
770777
case OVERRIDING:
771778
SPECIALIZATION_FAIL(STORE_ATTR, SPEC_FAIL_ATTR_OVERRIDING_DESCRIPTOR);
@@ -889,6 +896,9 @@ specialize_class_load_method(PyObject *owner, _Py_CODEUNIT *instr,
889896
PyObject *descr = NULL;
890897
DescriptorClassification kind = 0;
891898
kind = analyze_descriptor((PyTypeObject *)owner, name, &descr, 0);
899+
if (type_get_version((PyTypeObject *)owner, LOAD_METHOD) == 0) {
900+
return -1;
901+
}
892902
switch (kind) {
893903
case METHOD:
894904
case NON_DESCRIPTOR:
@@ -950,6 +960,9 @@ _Py_Specialize_LoadMethod(PyObject *owner, _Py_CODEUNIT *instr, PyObject *name)
950960
PyObject *descr = NULL;
951961
DescriptorClassification kind = 0;
952962
kind = analyze_descriptor(owner_cls, name, &descr, 0);
963+
if (type_get_version(owner_cls, LOAD_METHOD) == 0) {
964+
goto fail;
965+
}
953966
assert(descr != NULL || kind == ABSENT || kind == GETSET_OVERRIDDEN);
954967
if (kind != METHOD) {
955968
SPECIALIZATION_FAIL(LOAD_METHOD, load_method_fail_kind(kind));
@@ -1183,6 +1196,18 @@ function_kind(PyCodeObject *code) {
11831196
return SIMPLE_FUNCTION;
11841197
}
11851198

1199+
/* Returning 0 indicates a failure. */
1200+
static uint32_t
1201+
type_get_version(PyTypeObject *t, int opcode)
1202+
{
1203+
uint32_t version = t->tp_version_tag;
1204+
if (version == 0) {
1205+
SPECIALIZATION_FAIL(opcode, SPEC_FAIL_OUT_OF_VERSIONS);
1206+
return 0;
1207+
}
1208+
return version;
1209+
}
1210+
11861211
int
11871212
_Py_Specialize_BinarySubscr(
11881213
PyObject *container, PyObject *sub, _Py_CODEUNIT *instr)
@@ -1231,6 +1256,9 @@ _Py_Specialize_BinarySubscr(
12311256
SPECIALIZATION_FAIL(BINARY_SUBSCR, SPEC_FAIL_WRONG_NUMBER_ARGUMENTS);
12321257
goto fail;
12331258
}
1259+
if (type_get_version(cls, BINARY_SUBSCR) == 0) {
1260+
goto fail;
1261+
}
12341262
assert(cls->tp_version_tag != 0);
12351263
write_u32(cache->type_version, cls->tp_version_tag);
12361264
int version = _PyFunction_GetVersionForCurrentState(func);

0 commit comments

Comments
 (0)