Skip to content

gh-156399: Don't specialize LOAD_ATTR_MODULE for module subclasses - #156474

Merged
markshannon merged 1 commit into
python:mainfrom
skdas20:gh-156399-module-subclass-data-descriptor
Oct 22, 2026
Merged

markshannon merged 1 commit into
python:mainfrom
skdas20:gh-156399-module-subclass-data-descriptor

Conversation

@skdas20

@skdas20 skdas20 commented Aug 27, 2026 •

Copy link
Copy Markdown

Attribute loads on a types.ModuleType subclass are specialized to LOAD_ATTR_MODULE, which reads the module dictionary directly. Both the specialization site and the runtime guard tested only tp_getattro:

/* Python/specialize.c */
else if (Py_TYPE(owner)->tp_getattro == PyModule_Type.tp_getattro) {
    fail = specialize_module_load_attr(owner, instr, name);
}

/* Python/bytecodes.c */
EXIT_IF(Py_TYPE(owner_o)->tp_getattro != PyModule_Type.tp_getattro);

A subclass inherits PyModule_Type.tp_getattro unless it overrides __getattr__, so it satisfies both and takes the path. Neither check consults the type for a data descriptor, so once the instruction specializes the descriptor is bypassed and the dictionary value is returned instead. As noted in the issue, the guard was PyModule_CheckExact before 3.14, so subclasses never specialized.

This requires the exact module type in both places.

Why both

The runtime guard matters independently of the specialization site: one instruction can specialize against an exact module and later execute against a subclass instance, for example when a loop walks a list holding both. Tightening only specialize.c would still let a subclass reaching an already-specialized instruction bypass its descriptor.

Verification

Running the reproducer from the issue against a debug build of main:

['from descriptor', 'from dict', 'from dict', 'from dict']

and with this change:

['from descriptor', 'from descriptor', 'from descriptor', 'from descriptor']

test_load_attr_module_subclass_with_data_descriptor in Lib/test/test_opcache.py pins it, asserting both the descriptor result and that the code does not carry LOAD_ATTR_MODULE. It fails on unpatched main and passes with the change.

test_opcache, test_module, test_descr and test_importlib all pass. test_import reports 2 failures here (FilePermissionTests.test_creation_mode, PycacheTests.test_unwritable_directory), but those reproduce identically on an unpatched build of the same tree — they are container permission issues, not related to this change.

Note on scope

This restores the pre-3.14 behaviour of declining to specialize for subclasses, so there is no regression against 3.13. It does give up specialization for module subclasses that have no data descriptor. Keeping that case fast would mean proving no data descriptor exists for the name and guarding against one being added later, which _PyAttrCache has no type-version field for — so it looked like a larger change than a correctness fix should carry. Happy to pursue that instead if you'd prefer it.

Fixes #156399

Attribute loads on a types.ModuleType subclass were specialized to
LOAD_ATTR_MODULE, which reads the module dictionary directly. Both the
specialization site and the runtime guard tested

    Py_TYPE(owner)->tp_getattro == PyModule_Type.tp_getattro

which a subclass inherits unless it overrides __getattr__, so subclasses
took the path. A data descriptor defined on the subclass was then
silently bypassed once the instruction had specialized, and the value
from the module dictionary was returned instead. Before 3.14 the guard
was PyModule_CheckExact, so subclasses never specialized.

Require the exact module type in both places. The runtime guard matters
independently of the specialization site, because one instruction can be
specialized on an exact module and later executed against a subclass
instance.
@skdas20
skdas20 requested a review from markshannon as a code owner August 27, 2026 15:05
@python-cla-bot

python-cla-bot Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@eendebakpt

Copy link
Copy Markdown
Contributor

@eendebakpt eendebakpt added 3.14 bugs and security fixes 3.15 bugs and security fixes labels Oct 22, 2026
@skirpichev
skirpichev self-requested a review October 22, 2026 21:06

@markshannon markshannon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.
Thanks for fixing this.

@skirpichev skirpichev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping that case fast would mean proving no data descriptor exists for the name and guarding against one being added later, which _PyAttrCache has no type-version field for — so it looked like a larger change than a correctness fix should carry. Happy to pursue that instead if you'd prefer it.

I would appreciate if you try it.

But suspect that this will be much more expensive performance-wise. For example, PyProperty_Type can be used as the base type. So, you have to check against subtypes...

Probably there are no good workaround, except for PEP 726.

Should we then reopen #103951 after merging this or just give up?

@markshannon
markshannon merged commit 85599d3 into python:main Oct 22, 2026
87 of 88 checks passed
@markshannon

Copy link
Copy Markdown
Member

I don't think it is worth reopening #103951. If you (or anyone else) can come up with a fast check that generalizes but doesn't regress this, I'd be happy to review it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.14 bugs and security fixes 3.15 bugs and security fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LOAD_ATTR specialization for ModuleType subclasses bypasses data descriptors on the subclass (3.14 regression)

4 participants