Skip to content

Commit abbeb73

Browse files
authored
[mypyc] Fix memory leak on property setter call (#21095)
Calling a property setter method results in a memory leak because the setter method internally increfs the passed value and there is no corresponding decref on the caller side. This happens because the `SetAttr` op treats the source value as stolen which makes sense for regular attribute set. Change the op to not treat the source value as stolen for sets that call property setter methods so a decref is automatically generated after calling the setter. The leak can be observed by manually looking at memory use in `top` when running a loop like this one: ```python class Foo(): def __init__(self) -> None: self.attr = "unmodified" class A: def __init__(self) -> None: self._foo = Foo() @Property def foo(self) -> Foo: return self._foo @foo.setter def foo(self, val : Foo) -> None: self._foo = val a = A() while True: a.foo = Foo() ```
1 parent acfef9c commit abbeb73

File tree

4 files changed

+181
-6
lines changed

4 files changed

+181
-6
lines changed

mypyc/codegen/emitfunc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def visit_set_attr(self, op: SetAttr) -> None:
484484
rtype = op.class_type
485485
cl = rtype.class_ir
486486
attr_rtype, decl_cl = cl.attr_details(op.attr)
487-
if cl.get_method(op.attr):
487+
if op.is_propset:
488488
# Again, use vtable access for properties...
489489
assert not op.is_init and op.error_kind == ERR_FALSE, "%s %d %d %s" % (
490490
op.attr,

mypyc/ir/ops.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class to enable the new behavior. Sometimes adding a new abstract
3131

3232
from mypy_extensions import trait
3333

34+
from mypyc.common import PROPSET_PREFIX
3435
from mypyc.ir.deps import Dependency
3536
from mypyc.ir.rtypes import (
3637
RArray,
@@ -909,10 +910,7 @@ def accept(self, visitor: OpVisitor[T]) -> T:
909910

910911
@final
911912
class SetAttr(RegisterOp):
912-
"""obj.attr = src (for a native object)
913-
914-
Steals the reference to src.
915-
"""
913+
"""obj.attr = src (for a native object)"""
916914

917915
error_kind = ERR_FALSE
918916

@@ -928,6 +926,16 @@ def __init__(self, obj: Value, attr: str, src: Value, line: int) -> None:
928926
# and we don't use a setter
929927
self.is_init = False
930928

929+
cl = self.class_type.class_ir
930+
is_propset = False
931+
for ir in cl.mro:
932+
propset = ir.method_decls.get(PROPSET_PREFIX + attr)
933+
if propset is not None:
934+
is_propset = not propset.implicit
935+
break
936+
# If True, this op represents calling a property setter.
937+
self.is_propset = is_propset
938+
931939
def mark_as_initializer(self) -> None:
932940
self.is_init = True
933941
self.error_kind = ERR_NEVER
@@ -940,6 +948,10 @@ def set_sources(self, new: list[Value]) -> None:
940948
self.obj, self.src = new
941949

942950
def stolen(self) -> list[Value]:
951+
# The property setter method increfs the passed value so don't treat it as a steal
952+
# to avoid leaking.
953+
if self.is_propset:
954+
return []
943955
return [self.src]
944956

945957
def accept(self, visitor: OpVisitor[T]) -> T:

mypyc/test-data/refcount.test

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,3 +1931,131 @@ L0:
19311931
r10 = unborrow r8
19321932
x = r10
19331933
return x
1934+
1935+
[case testPropertySetterCallWithRefcountedObject]
1936+
class Foo:
1937+
def __init__(self) -> None:
1938+
self.attr = "unmodified"
1939+
1940+
class A:
1941+
def __init__(self) -> None:
1942+
self._foo = Foo()
1943+
1944+
@property
1945+
def foo(self) -> Foo:
1946+
return self._foo
1947+
1948+
@foo.setter
1949+
def foo(self, val : Foo) -> None:
1950+
self._foo = val
1951+
1952+
def f(a: A):
1953+
a.foo = Foo()
1954+
[out]
1955+
def Foo.__init__(self):
1956+
self :: __main__.Foo
1957+
r0 :: str
1958+
L0:
1959+
r0 = 'unmodified'
1960+
inc_ref r0
1961+
self.attr = r0
1962+
return 1
1963+
def A.__init__(self):
1964+
self :: __main__.A
1965+
r0 :: __main__.Foo
1966+
L0:
1967+
r0 = Foo()
1968+
self._foo = r0
1969+
return 1
1970+
def A.foo(self):
1971+
self :: __main__.A
1972+
r0 :: __main__.Foo
1973+
L0:
1974+
r0 = self._foo
1975+
return r0
1976+
def A.__mypyc_setter__foo(self, val):
1977+
self :: __main__.A
1978+
val :: __main__.Foo
1979+
r0 :: bool
1980+
L0:
1981+
inc_ref val
1982+
self._foo = val; r0 = is_error
1983+
return 1
1984+
def f(a):
1985+
a :: __main__.A
1986+
r0 :: __main__.Foo
1987+
r1 :: bool
1988+
r2 :: object
1989+
L0:
1990+
r0 = Foo()
1991+
a.foo = r0; r1 = is_error
1992+
dec_ref r0
1993+
r2 = box(None, 1)
1994+
inc_ref r2
1995+
return r2
1996+
1997+
[case testPropertySetterCallWithNonRefcountedObject_64bit]
1998+
from mypy_extensions import i64
1999+
2000+
class A:
2001+
def __init__(self) -> None:
2002+
self._foo = i64(0)
2003+
2004+
@property
2005+
def foo(self) -> i64:
2006+
return self._foo
2007+
2008+
@foo.setter
2009+
def foo(self, val : i64) -> None:
2010+
self._foo = val
2011+
2012+
def f(a: A, i: int):
2013+
a.foo = i64(i)
2014+
[out]
2015+
def A.__init__(self):
2016+
self :: __main__.A
2017+
L0:
2018+
self._foo = 0
2019+
return 1
2020+
def A.foo(self):
2021+
self :: __main__.A
2022+
r0 :: i64
2023+
L0:
2024+
r0 = self._foo
2025+
return r0
2026+
def A.__mypyc_setter__foo(self, val):
2027+
self :: __main__.A
2028+
val :: i64
2029+
r0 :: bool
2030+
L0:
2031+
self._foo = val; r0 = is_error
2032+
return 1
2033+
def f(a, i):
2034+
a :: __main__.A
2035+
i :: int
2036+
r0 :: native_int
2037+
r1 :: bit
2038+
r2, r3 :: i64
2039+
r4 :: ptr
2040+
r5 :: c_ptr
2041+
r6 :: i64
2042+
r7 :: bool
2043+
r8 :: object
2044+
L0:
2045+
r0 = i & 1
2046+
r1 = r0 == 0
2047+
if r1 goto L1 else goto L2 :: bool
2048+
L1:
2049+
r2 = i >> 1
2050+
r3 = r2
2051+
goto L3
2052+
L2:
2053+
r4 = i ^ 1
2054+
r5 = r4
2055+
r6 = CPyLong_AsInt64(r5)
2056+
r3 = r6
2057+
L3:
2058+
a.foo = r3; r7 = is_error
2059+
r8 = box(None, 1)
2060+
inc_ref r8
2061+
return r8

mypyc/test-data/run-classes.test

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2160,8 +2160,40 @@ class H:
21602160

21612161
[file other.py]
21622162
# Run in both interpreted and compiled mode
2163+
import gc
2164+
2165+
from native import A, B, C, D, E, F, Foo, G
2166+
2167+
def test_leaks() -> None:
2168+
a = A()
2169+
2170+
gc.collect()
2171+
before = gc.get_objects()
2172+
for _ in range(100):
2173+
a.foo = Foo()
2174+
gc.collect()
2175+
after = gc.get_objects()
2176+
2177+
diff = len(after) - len(before)
2178+
# Small difference is expected, if the property setter call was leaking objects
2179+
# we would get a difference similar to the number of iterations above.
2180+
assert diff <= 2, diff
21632181

2164-
from native import A, B, C, D, E, F, G
2182+
def set_foo(foo: Foo) -> None:
2183+
a = A()
2184+
a.foo = foo
2185+
2186+
def test_use_after_passing_to_prop_setter() -> None:
2187+
foo = Foo()
2188+
2189+
foo.attr = "before"
2190+
assert foo.attr == "before", foo.attr
2191+
2192+
set_foo(foo)
2193+
assert foo.attr == "before", foo.attr
2194+
2195+
foo.attr = "after"
2196+
assert foo.attr == "after", foo.attr
21652197

21662198
a = A()
21672199
assert a.x == 0
@@ -2195,6 +2227,9 @@ g = G(4)
21952227
g.x = 20
21962228
assert g.x == 20
21972229

2230+
test_leaks()
2231+
test_use_after_passing_to_prop_setter()
2232+
21982233
[file driver.py]
21992234
# Run the tests in both interpreted and compiled mode
22002235
import other

0 commit comments

Comments
 (0)