Repository navigation
Use active builtins consistently in descriptor and method reductions #9057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import _pickle | ||
| import io | ||
| import sys | ||
| from types import MappingProxyType, MethodType | ||
|
|
||
| from testutils import assert_raises | ||
|
|
||
|
|
@@ -35,3 +36,83 @@ | |
| else: | ||
| expected = f"{name} exceeds system's maximum size of {sys.maxsize} bytes" | ||
| assert str(caught.exception) == expected | ||
|
|
||
|
|
||
| # Reduction returns getattr from the active frame's builtins without calling it. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This added regression block introduces new test data, execution logic, and assertions in a test file, while the repository permits test-file edits only for adding or removing expected-failure decorators; the test additions therefore need to be removed unless the maintainers revise that policy first. AGENTS.md reference: AGENTS.md:L275-L279 Useful? React with 👍 / 👎. |
||
| class ReductionOwner: | ||
| __slots__ = ("slot",) | ||
|
|
||
| def method(self): | ||
| pass | ||
|
|
||
|
|
||
| owner = ReductionOwner() | ||
| items = [] | ||
| replacement_getattr = object() | ||
| for descriptor, receiver, name in ( | ||
| (object.__str__, object, "__str__"), | ||
| (str.join, str, "join"), | ||
| (io.BytesIO.getvalue, io.BytesIO, "getvalue"), | ||
| (ReductionOwner.slot, ReductionOwner, "slot"), | ||
| (owner.__str__, owner, "__str__"), | ||
| (items.append, items, "append"), | ||
| (dict.fromkeys, dict, "fromkeys"), | ||
| (owner.method, owner, "method"), | ||
| ): | ||
| for builtins_type in (dict, MappingProxyType): | ||
| namespace = { | ||
| "__builtins__": builtins_type({"getattr": replacement_getattr}), | ||
| "descriptor": descriptor, | ||
| } | ||
| exec("reduced = descriptor.__reduce__()", namespace) | ||
| assert namespace["reduced"] == (replacement_getattr, (receiver, name)) | ||
|
|
||
| namespace["__builtins__"] = builtins_type({}) | ||
| with assert_raises(AttributeError) as caught: | ||
| exec("descriptor.__reduce__()", namespace) | ||
| assert caught.exception.args == ("getattr",) | ||
|
|
||
| # Module-bound functions reduce to a name without looking up getattr. | ||
| namespace = {"__builtins__": {}, "function": len} | ||
| exec("reduced = function.__reduce__()", namespace) | ||
| assert namespace["reduced"] == "len" | ||
|
|
||
|
|
||
| class MissingBuiltin(dict): | ||
| def __getitem__(self, key): | ||
| raise KeyError(key) | ||
|
|
||
| def __missing__(self, key): | ||
| raise AssertionError("must not retry __missing__ after custom __getitem__") | ||
|
|
||
|
|
||
| for descriptor, name in ( | ||
| (object.__str__, "getattr"), | ||
| (iter([1]), "iter"), | ||
| (reversed([1]), "reversed"), | ||
| ): | ||
| namespace = {"__builtins__": MissingBuiltin(), "descriptor": descriptor} | ||
| with assert_raises(AttributeError) as caught: | ||
| exec("descriptor.__reduce__()", namespace) | ||
| assert caught.exception.args == (name,) | ||
|
|
||
|
|
||
| name_error = RuntimeError("function name first") | ||
|
|
||
|
|
||
| class NamedCallable: | ||
| def __call__(self, *args): | ||
| pass | ||
|
|
||
| @property | ||
| def __name__(self): | ||
| raise name_error | ||
|
|
||
|
|
||
| namespace = { | ||
| "__builtins__": {}, | ||
| "method": MethodType(NamedCallable(), owner), | ||
| } | ||
| with assert_raises(RuntimeError) as caught: | ||
| exec("method.__reduce__()", namespace) | ||
| assert caught.exception is name_error | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tell me why this is important. and also this pattern is looking like need to be applied more places. if it does, patch and include here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is observable compatibility of the reconstruction recipe. With a custom active builtins mapping, CPython returns that mapping’s exact getattr object from reduce, without calling it; if the entry is absent, reduction raises AttributeError("getattr"). Reading vm.builtins instead returns a different callable and hides that lookup failure. This is not a sandbox-security claim.
You are right about the other sites. Exact CPython 3.14.8 source and bounded reference/native checks identify the same issue in method/member descriptors, bound native methods, Python bound methods, and method wrappers. I am including those fixes here, with stored defining-type handling and name lookup order preserved, and expanding the existing parameterized Python regression. Module-bound functions still return a name. Classmethod/getset descriptors and pickle’s separately cached getattr have different contracts and are not being changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Included in 4944d19: all five remaining reducer sites now use active builtins; method descriptors retain their actual defining type; Python bound methods keep name lookup first. The helper now performs one generic lookup for dictionary subclasses, preserving inherited missing without an extra fallback after overridden getitem. The original wrapper assertions remain covered in the expanded table.
Exact CPython 3.14.7/3.14.8 reference matrices and the native candidate agree, including callback counts and exception order. The full pickle snippet, both snippet pytest entries, four unchanged canonical method/iterator tests, normal hooks, and VM/CAPI Clippy pass. Two pre-existing C-method pickle expected failures remain unchanged. Module-bound functions, classmethod/getset descriptors, and pickle’s cached getattr retain their distinct contracts. New-head CI is running.