Skip to content

Commit 5374fec

Browse files
authored
[mypyc] Fix function wrapper memory leak (#21654)
The `name` and `code` variables passed to the function wrapper init function are increfed inside it before storing them in the function wrapper object but they are not decrefed after the init function returns which creates a leak. Change the init function to instead steal `name` and `code` and document it.
1 parent 2433566 commit 5374fec

2 files changed

Lines changed: 48 additions & 7 deletions

File tree

mypyc/lib-rt/function_wrapper.c

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ static PyObject* CPyFunction_Vectorcall(PyObject *func, PyObject *const *args, s
196196
}
197197

198198

199+
// Steals ml, name, and code. Borrows module.
199200
static CPyFunction* CPyFunction_Init(CPyFunction *op, PyMethodDef *ml, PyObject* name,
200201
PyObject *module, PyObject* code, bool set_self) {
201202
PyCFunctionObject *cf = (PyCFunctionObject *)op;
@@ -206,12 +207,10 @@ static CPyFunction* CPyFunction_Init(CPyFunction *op, PyMethodDef *ml, PyObject*
206207
Py_XINCREF(module);
207208
cf->m_module = module;
208209

209-
Py_INCREF(name);
210210
op->func_name = name;
211211

212212
((PyCMethodObject *)op)->mm_class = NULL;
213213

214-
Py_XINCREF(code);
215214
op->func_code = code;
216215

217216
CPyFunction_func_vectorcall(op) = CPyFunction_Vectorcall;
@@ -243,7 +242,7 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu
243242
PyCFunction func, int func_flags, const char *func_doc,
244243
int first_line, int code_flags, bool has_self_arg) {
245244
PyMethodDef *method = NULL;
246-
PyObject *code = NULL, *op = NULL;
245+
PyObject *code = NULL, *name = NULL, *op = NULL;
247246
bool set_self = false;
248247

249248
#ifdef Py_GIL_DISABLED
@@ -280,18 +279,24 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu
280279
if (unlikely(!code)) {
281280
goto err;
282281
}
282+
name = PyUnicode_FromString(funcname);
283+
if (unlikely(!name)) {
284+
goto err;
285+
}
283286

284287
// Set m_self inside the function wrapper only if the wrapped function has no self arg
285288
// to pass m_self as the self arg when the function is called.
286289
// When the function has a self arg, it will come in the args vector passed to the
287290
// vectorcall handler.
288291
set_self = !has_self_arg;
289-
op = (PyObject *)CPyFunction_Init(PyObject_GC_New(CPyFunction, CPyFunctionType),
290-
method, PyUnicode_FromString(funcname), module,
291-
code, set_self);
292-
if (unlikely(!op)) {
292+
CPyFunction *raw = PyObject_GC_New(CPyFunction, CPyFunctionType);
293+
if (unlikely(!raw)) {
293294
goto err;
294295
}
296+
op = (PyObject *)CPyFunction_Init(raw, method, name, module, code, set_self);
297+
method = NULL;
298+
name = NULL;
299+
code = NULL;
295300
PyObject_GC_Track(op);
296301
return op;
297302

@@ -300,5 +305,7 @@ PyObject* CPyFunction_New(PyObject *module, const char *filename, const char *fu
300305
if (method) {
301306
PyMem_Free(method);
302307
}
308+
Py_XDECREF(name);
309+
Py_XDECREF(code);
303310
return NULL;
304311
}

mypyc/test-data/run-async.test

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,13 +1416,16 @@ def run(x: object) -> object: ...
14161416

14171417
[case testAsyncIntrospection]
14181418
import asyncio
1419+
import gc
14191420
import inspect
14201421
import sys
14211422
import weakref
14221423

14231424
from functools import wraps
14241425
from typing import Any, Callable, TypeVar, cast
14251426

1427+
from testutil import is_gil_disabled
1428+
14261429
def identity(val: int) -> int:
14271430
return val
14281431

@@ -1589,6 +1592,37 @@ def test_nested() -> None:
15891592
assert is_coroutine(nested_wrapped_async)
15901593
assert asyncio.run(nested_wrapped_async()) == 4
15911594

1595+
def test_async_function_wrapper_code_refcount() -> None:
1596+
if is_gil_disabled():
1597+
# On free-threaded builds the code object might be immortal, so the ref count test doesn't work.
1598+
return
1599+
code = getattr(identity_async, "__code__")
1600+
getrefcount = getattr(sys, "getrefcount")
1601+
# getrefcount sees the local code variable plus the wrapper-owned reference.
1602+
assert getrefcount(code) == 2, getrefcount(code)
1603+
1604+
def test_nested_async_function_wrapper_code_refcount() -> None:
1605+
if is_gil_disabled():
1606+
# On free-threaded builds the code object might be immortal, so the ref count test doesn't work.
1607+
return
1608+
def make_nested() -> Any:
1609+
async def nested_refcounted() -> int:
1610+
return 1
1611+
1612+
return nested_refcounted
1613+
1614+
getrefcount = getattr(sys, "getrefcount")
1615+
fn = make_nested()
1616+
code = getattr(fn, "__code__")
1617+
before = getrefcount(code)
1618+
assert asyncio.run(fn()) == 1
1619+
1620+
del fn
1621+
gc.collect()
1622+
after = getrefcount(code)
1623+
assert before == after + 1, (before, after)
1624+
assert after == 1, after
1625+
15921626
[file asyncio/__init__.pyi]
15931627
def run(x: object) -> object: ...
15941628

0 commit comments

Comments
 (0)