From bcb872a2e406372c0c1fce2298e6728b715f7fd4 Mon Sep 17 00:00:00 2001 From: knQzx <75641500+knQzx@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:50:44 +0200 Subject: [PATCH] Reject arbitrary attribute assignment on bound methods A bound method is a types.MethodType, which does not override __setattr__/__delattr__, so assigning an arbitrary attribute to it fails at runtime. Pyrefly permitted it: lookup_magic_dunder_attr1 had no BoundMethod arm, so the base fell through to the general lookup and resolved __setattr__/__delattr__ via MethodType.__getattr__ (which returns Any), making the assignment look valid. Add a BoundMethod arm mirroring the ClassInstance handling: when the mutating dunder is inherited from object, report the attribute as not-found so the set/delete is rejected. Attribute reads are unchanged. Closes #4163 --- pyrefly/lib/alt/attr.rs | 18 ++++++++++++++++++ pyrefly/lib/test/attributes.rs | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pyrefly/lib/alt/attr.rs b/pyrefly/lib/alt/attr.rs index f56dd13c35..461fb47397 100644 --- a/pyrefly/lib/alt/attr.rs +++ b/pyrefly/lib/alt/attr.rs @@ -2017,6 +2017,24 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { { acc.not_found(NotFoundOn::ClassInstance(cls.class_object().clone(), base)) } + // A bound method is a `types.MethodType`, which does not override + // `__setattr__`/`__delattr__`, so it does not accept arbitrary attribute + // assignment (this fails at runtime). Without this arm the base would fall + // through to the general lookup and resolve the dunder via + // `MethodType.__getattr__`, incorrectly permitting the assignment. + AttributeBase1::BoundMethod(_) + if (*dunder_name == dunder::SETATTR || *dunder_name == dunder::DELATTR) + && self.field_is_inherited_from( + self.stdlib.method_type().class_object(), + dunder_name, + (ModuleName::builtins().as_str(), "object"), + ) => + { + acc.not_found(NotFoundOn::ClassInstance( + self.stdlib.method_type().class_object().clone(), + base, + )) + } AttributeBase1::ShapedArrayInstance(tensor) if (*dunder_name == dunder::SETATTR || *dunder_name == dunder::DELATTR diff --git a/pyrefly/lib/test/attributes.rs b/pyrefly/lib/test/attributes.rs index 437dc6640e..3ffef0ce8a 100644 --- a/pyrefly/lib/test/attributes.rs +++ b/pyrefly/lib/test/attributes.rs @@ -3055,3 +3055,16 @@ class C: x = untyped(1) # E: implicitly inferred to be `Any` "#, ); + +testcase!( + test_bound_method_no_arbitrary_attr_set, + r#" +class Foo: + def real_method(self) -> None: ... +f: Foo = Foo() +f.real_method.test = None # E: has no attribute `test` +del f.real_method.test # E: has no attribute `test` +# Known method attributes are still accessible. +name: str = f.real_method.__name__ +"#, +);