https://github.com/python/cpython/commit/85599d33d734c9e586f0dd9b7fc87ec0b9f2a6b5
commit: 85599d33d734c9e586f0dd9b7fc87ec0b9f2a6b5
branch: main
author: Sumit Kumar Das <[email protected]>
committer: markshannon <[email protected]>
date: 2026-10-22T16:53:35-07:00
summary:

gh-156399: Don't specialize LOAD_ATTR_MODULE for module subclasses (GH-156474)

Fixes https://github.com/python/cpython/issues/156399

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-00-00-00.gh-issue-156399.aB3xYz.rst
M Lib/test/test_opcache.py
M Modules/_testinternalcapi/test_cases.c.h
M Python/bytecodes.c
M Python/executor_cases.c.h
M Python/generated_cases.c.h
M Python/specialize.c

diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py
index 357956a4b661b3d..5f7c15f93a8332a 100644
--- a/Lib/test/test_opcache.py
+++ b/Lib/test/test_opcache.py
@@ -2326,6 +2326,35 @@ def load_module_attr_missing():
         finally:
             sys.modules.pop("test_module_with_getattr", None)
 
+    @cpython_only
+    @requires_specialization
+    def test_load_attr_module_subclass_with_data_descriptor(self):
+        # gh-156399: a types.ModuleType subclass inherits
+        # PyModule_Type.tp_getattro, so guarding specialization on tp_getattro
+        # alone let subclasses take LOAD_ATTR_MODULE. That reads the module
+        # dict directly, silently bypassing a data descriptor defined on the
+        # subclass once the instruction had specialized.
+        class Descriptor:
+            def __get__(self, instance, owner=None):
+                return "from descriptor"
+
+            def __set__(self, instance, value):
+                instance.__dict__["attr"] = value
+
+        class Module(types.ModuleType):
+            attr = Descriptor()
+
+        module = Module("test_module_subclass")
+        module.__dict__["attr"] = "from dict"
+
+        def load_module_attr():
+            return module.attr
+
+        for _ in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
+            self.assertEqual(load_module_attr(), "from descriptor")
+
+        self.assert_no_opcode(load_module_attr, "LOAD_ATTR_MODULE")
+
     @cpython_only
     @requires_specialization
     def test_specialized_iter_doesnt_skip_send_check(self):
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-00-00-00.gh-issue-156399.aB3xYz.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-00-00-00.gh-issue-156399.aB3xYz.rst
new file mode 100644
index 000000000000000..7680a16bfe6fd7f
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-00-00-00.gh-issue-156399.aB3xYz.rst
@@ -0,0 +1,4 @@
+Fix a regression where attribute access on a :class:`types.ModuleType`
+subclass could bypass a data descriptor defined on the subclass. The
+``LOAD_ATTR_MODULE`` specialization guarded on ``tp_getattro``, which
+subclasses inherit, and then read the module dictionary directly.
diff --git a/Modules/_testinternalcapi/test_cases.c.h 
b/Modules/_testinternalcapi/test_cases.c.h
index 2c5af6afd17f004..8e7ba1f69553dda 100644
--- a/Modules/_testinternalcapi/test_cases.c.h
+++ b/Modules/_testinternalcapi/test_cases.c.h
@@ -9230,7 +9230,7 @@
                 uint32_t dict_version = read_u32(&this_instr[2].cache);
                 uint16_t index = read_u16(&this_instr[4].cache);
                 PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner);
-                if (Py_TYPE(owner_o)->tp_getattro != 
PyModule_Type.tp_getattro) {
+                if (!PyModule_CheckExact(owner_o)) {
                     UPDATE_MISS_STATS(LOAD_ATTR);
                     assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR));
                     JUMP_TO_PREDICTED(LOAD_ATTR);
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 484820911309007..42ad83daa1ea461 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -2888,7 +2888,7 @@ dummy_func(
 
         op(_LOAD_ATTR_MODULE, (dict_version/2, index/1, owner -- attr, o)) {
             PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner);
-            EXIT_IF(Py_TYPE(owner_o)->tp_getattro != 
PyModule_Type.tp_getattro);
+            EXIT_IF(!PyModule_CheckExact(owner_o));
             PyDictObject *dict = (PyDictObject *)((PyModuleObject 
*)owner_o)->md_dict;
             assert(dict != NULL);
             PyDictKeysObject *keys = FT_ATOMIC_LOAD_PTR_ACQUIRE(dict->ma_keys);
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 78d97c36de4324b..bc85ccd9ddb4fc1 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -12423,7 +12423,7 @@
             uint32_t dict_version = (uint32_t)CURRENT_OPERAND0_32();
             uint16_t index = (uint16_t)CURRENT_OPERAND1_16();
             PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner);
-            if (Py_TYPE(owner_o)->tp_getattro != PyModule_Type.tp_getattro) {
+            if (!PyModule_CheckExact(owner_o)) {
                 UOP_STAT_INC(uopcode, miss);
                 _tos_cache0 = owner;
                 SET_CURRENT_CACHED_VALUES(1);
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index 21f3a63fb715f87..ee9b64107bb9f24 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -9229,7 +9229,7 @@
                 uint32_t dict_version = read_u32(&this_instr[2].cache);
                 uint16_t index = read_u16(&this_instr[4].cache);
                 PyObject *owner_o = PyStackRef_AsPyObjectBorrow(owner);
-                if (Py_TYPE(owner_o)->tp_getattro != 
PyModule_Type.tp_getattro) {
+                if (!PyModule_CheckExact(owner_o)) {
                     UPDATE_MISS_STATS(LOAD_ATTR);
                     assert(_PyOpcode_Deopt[opcode] == (LOAD_ATTR));
                     JUMP_TO_PREDICTED(LOAD_ATTR);
diff --git a/Python/specialize.c b/Python/specialize.c
index fc5aef67301f2a1..067bae59f6073b7 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -1011,7 +1011,7 @@ _Py_Specialize_LoadAttr(_PyStackRef owner_st, 
_Py_CODEUNIT *instr, PyObject *nam
         SPECIALIZATION_FAIL(LOAD_ATTR, SPEC_FAIL_OTHER);
         fail = true;
     }
-    else if (Py_TYPE(owner)->tp_getattro == PyModule_Type.tp_getattro) {
+    else if (PyModule_CheckExact(owner)) {
         fail = specialize_module_load_attr(owner, instr, name);
     }
     else if (PyType_Check(owner)) {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to