Skip to content

Commit 32e0ca8

Browse files
committed
[mypyc] Fix memory leak when borrowing property getter return values
`is_native_attr_ref()` uses `has_attr(name) and not get_method(name)` to decide if an attribute access can borrow. `has_attr()` is populated during preparation (always complete), but `get_method()` checks `ir.methods` which is populated during compilation. When cross-module classes haven't been compiled yet, `get_method()` returns None for property getters, incorrectly allowing borrowing. Property getters return new owned references, so skipping the DECREF leaks one reference per call. The fix checks `ir.attributes` directly (struct fields only, always populated during preparation). Properties are never in `ir.attributes`, so they always return False. This is the getter-side counterpart to python#21095.
1 parent 19cf4d9 commit 32e0ca8

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

mypyc/irbuild/builder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1553,8 +1553,7 @@ def is_native_attr_ref(self, expr: MemberExpr) -> bool:
15531553
return (
15541554
isinstance(obj_rtype, RInstance)
15551555
and obj_rtype.class_ir.is_ext_class
1556-
and obj_rtype.class_ir.has_attr(expr.name)
1557-
and not obj_rtype.class_ir.get_method(expr.name)
1556+
and any(expr.name in ir.attributes for ir in obj_rtype.class_ir.mro)
15581557
)
15591558

15601559
def mark_block_unreachable(self) -> None:

mypyc/test-data/run-classes.test

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5925,3 +5925,47 @@ assert NonExtDict.BASE == {"x": 1}
59255925
assert NonExtDict.EXTENDED == {"x": 1, "y": 2}
59265926

59275927
assert NonExtChained.Z == {10, 20, 30}
5928+
5929+
[case testPropertyGetterLeak]
5930+
class Bar:
5931+
pass
5932+
5933+
class Foo:
5934+
def __init__(self) -> None:
5935+
self.obj: object = Bar()
5936+
5937+
@property
5938+
def val(self) -> object:
5939+
return self.obj
5940+
5941+
[file other.py]
5942+
import gc
5943+
from native import Foo, Bar
5944+
5945+
def check(foo: Foo) -> bool:
5946+
return isinstance(foo.val, Bar)
5947+
5948+
def test_property_getter_no_leak() -> None:
5949+
foo = Foo()
5950+
gc.collect()
5951+
before = gc.get_objects()
5952+
for _ in range(100):
5953+
check(foo)
5954+
gc.collect()
5955+
after = gc.get_objects()
5956+
diff = len(after) - len(before)
5957+
assert diff <= 2, diff
5958+
5959+
test_property_getter_no_leak()
5960+
5961+
[file driver.py]
5962+
import sys
5963+
from other import check
5964+
from native import Foo
5965+
5966+
foo = Foo()
5967+
init = sys.getrefcount(foo.obj)
5968+
for _ in range(100):
5969+
check(foo)
5970+
after = sys.getrefcount(foo.obj)
5971+
assert after - init == 0, f"Leaked {after - init} refs"

0 commit comments

Comments
 (0)