Skip to content

Commit

Permalink
Issue #18619: Fix atexit leaking callbacks registered from sub-interp…
Browse files Browse the repository at this point in the history
…reters, and make it GC-aware.
  • Loading branch information
pitrou committed Aug 1, 2013
1 parent 7a2572c commit 2d350fd
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 44 deletions.
42 changes: 40 additions & 2 deletions Lib/test/test_atexit.py
Expand Up @@ -2,6 +2,7 @@
import unittest
import io
import atexit
import _testcapi
from test import support

### helpers
Expand All @@ -23,7 +24,9 @@ def raise1():
def raise2():
raise SystemError

class TestCase(unittest.TestCase):

class GeneralTest(unittest.TestCase):

def setUp(self):
self.save_stdout = sys.stdout
self.save_stderr = sys.stderr
Expand Down Expand Up @@ -122,8 +125,43 @@ def test_bound_methods(self):
self.assertEqual(l, [5])


class SubinterpreterTest(unittest.TestCase):

def test_callbacks_leak(self):
# This test shows a leak in refleak mode if atexit doesn't
# take care to free callbacks in its per-subinterpreter module
# state.
n = atexit._ncallbacks()
code = r"""if 1:
import atexit
def f():
pass
atexit.register(f)
del atexit
"""
ret = _testcapi.run_in_subinterp(code)
self.assertEqual(ret, 0)
self.assertEqual(atexit._ncallbacks(), n)

def test_callbacks_leak_refcycle(self):
# Similar to the above, but with a refcycle through the atexit
# module.
n = atexit._ncallbacks()
code = r"""if 1:
import atexit
def f():
pass
atexit.register(f)
atexit.__atexit = atexit
"""
ret = _testcapi.run_in_subinterp(code)
self.assertEqual(ret, 0)
self.assertEqual(atexit._ncallbacks(), n)


def test_main():
support.run_unittest(TestCase)
support.run_unittest(__name__)


if __name__ == "__main__":
test_main()
3 changes: 3 additions & 0 deletions Misc/NEWS
Expand Up @@ -179,6 +179,9 @@ Core and Builtins
Library
-------

- Issue #18619: Fix atexit leaking callbacks registered from sub-interpreters,
and make it GC-aware.

- Issue #15699: The readline module now uses PEP 3121-style module
initialization, so as to reclaim allocated resources (Python callbacks)
at shutdown. Original patch by Robin Schreiber.
Expand Down
121 changes: 79 additions & 42 deletions Modules/atexitmodule.c
Expand Up @@ -10,8 +10,6 @@

/* Forward declaration (for atexit_cleanup) */
static PyObject *atexit_clear(PyObject*, PyObject*);
/* Forward declaration (for atexit_callfuncs) */
static void atexit_cleanup(PyObject*);
/* Forward declaration of module object */
static struct PyModuleDef atexitmodule;

Expand All @@ -33,6 +31,35 @@ typedef struct {
#define GET_ATEXIT_STATE(mod) ((atexitmodule_state*)PyModule_GetState(mod))


static void
atexit_delete_cb(atexitmodule_state *modstate, int i)
{
atexit_callback *cb;

cb = modstate->atexit_callbacks[i];
modstate->atexit_callbacks[i] = NULL;
Py_DECREF(cb->func);
Py_DECREF(cb->args);
Py_XDECREF(cb->kwargs);
PyMem_Free(cb);
}

/* Clear all callbacks without calling them */
static void
atexit_cleanup(atexitmodule_state *modstate)
{
atexit_callback *cb;
int i;
for (i = 0; i < modstate->ncallbacks; i++) {
cb = modstate->atexit_callbacks[i];
if (cb == NULL)
continue;

atexit_delete_cb(modstate, i);
}
modstate->ncallbacks = 0;
}

/* Installed into pythonrun.c's atexit mechanism */

static void
Expand Down Expand Up @@ -78,34 +105,12 @@ atexit_callfuncs(void)
}
}

atexit_cleanup(module);
atexit_cleanup(modstate);

if (exc_type)
PyErr_Restore(exc_type, exc_value, exc_tb);
}

static void
atexit_delete_cb(PyObject *self, int i)
{
atexitmodule_state *modstate;
atexit_callback *cb;

modstate = GET_ATEXIT_STATE(self);
cb = modstate->atexit_callbacks[i];
modstate->atexit_callbacks[i] = NULL;
Py_DECREF(cb->func);
Py_DECREF(cb->args);
Py_XDECREF(cb->kwargs);
PyMem_Free(cb);
}

static void
atexit_cleanup(PyObject *self)
{
PyObject *r = atexit_clear(self, NULL);
Py_DECREF(r);
}

/* ===================================================================== */
/* Module methods. */

Expand Down Expand Up @@ -193,29 +198,59 @@ Clear the list of previously registered exit functions.");

static PyObject *
atexit_clear(PyObject *self, PyObject *unused)
{
atexit_cleanup(GET_ATEXIT_STATE(self));
Py_RETURN_NONE;
}

PyDoc_STRVAR(atexit_ncallbacks__doc__,
"_ncallbacks() -> int\n\
\n\
Return the number of registered exit functions.");

static PyObject *
atexit_ncallbacks(PyObject *self, PyObject *unused)
{
atexitmodule_state *modstate;
atexit_callback *cb;
int i;

modstate = GET_ATEXIT_STATE(self);

return PyLong_FromSsize_t(modstate->ncallbacks);
}

static int
atexit_m_traverse(PyObject *self, visitproc visit, void *arg)
{
int i;
atexitmodule_state *modstate;

modstate = GET_ATEXIT_STATE(self);
for (i = 0; i < modstate->ncallbacks; i++) {
cb = modstate->atexit_callbacks[i];
atexit_callback *cb = modstate->atexit_callbacks[i];
if (cb == NULL)
continue;

atexit_delete_cb(self, i);
Py_VISIT(cb->func);
Py_VISIT(cb->args);
Py_VISIT(cb->kwargs);
}
modstate->ncallbacks = 0;
Py_RETURN_NONE;
return 0;
}

static int
atexit_m_clear(PyObject *self)
{
atexitmodule_state *modstate;
modstate = GET_ATEXIT_STATE(self);
atexit_cleanup(modstate);
return 0;
}

static void
atexit_free(PyObject *m)
{
atexitmodule_state *modstate;
modstate = GET_ATEXIT_STATE(m);
atexit_cleanup(modstate);
PyMem_Free(modstate->atexit_callbacks);
}

Expand Down Expand Up @@ -246,7 +281,7 @@ atexit_unregister(PyObject *self, PyObject *func)
if (eq < 0)
return NULL;
if (eq)
atexit_delete_cb(self, i);
atexit_delete_cb(modstate, i);
}
Py_RETURN_NONE;
}
Expand All @@ -260,6 +295,8 @@ static PyMethodDef atexit_methods[] = {
atexit_unregister__doc__},
{"_run_exitfuncs", (PyCFunction) atexit_run_exitfuncs, METH_NOARGS,
atexit_run_exitfuncs__doc__},
{"_ncallbacks", (PyCFunction) atexit_ncallbacks, METH_NOARGS,
atexit_ncallbacks__doc__},
{NULL, NULL} /* sentinel */
};

Expand All @@ -275,15 +312,15 @@ Two public functions, register and unregister, are defined.\n\


static struct PyModuleDef atexitmodule = {
PyModuleDef_HEAD_INIT,
"atexit",
atexit__doc__,
sizeof(atexitmodule_state),
atexit_methods,
NULL,
NULL,
NULL,
(freefunc)atexit_free
PyModuleDef_HEAD_INIT,
"atexit",
atexit__doc__,
sizeof(atexitmodule_state),
atexit_methods,
NULL,
atexit_m_traverse,
atexit_m_clear,
(freefunc)atexit_free
};

PyMODINIT_FUNC
Expand Down

0 comments on commit 2d350fd

Please sign in to comment.