Skip to content

Commit

Permalink
gh-91636: Don't clear required fields of function objects (GH-91651)
Browse files Browse the repository at this point in the history
  • Loading branch information
sweeneyde authored Apr 21, 2022
1 parent 615b24c commit f2b4e45
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 3 deletions.
67 changes: 67 additions & 0 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,73 @@ def test_function(self):
del d
self.assertEqual(gc.collect(), 2)

def test_function_tp_clear_leaves_consistent_state(self):
# https://github.com/python/cpython/issues/91636
code = """if 1:
import gc
import weakref
class LateFin:
__slots__ = ('ref',)
def __del__(self):
# 8. Now `latefin`'s finalizer is called. Here we
# obtain a reference to `func`, which is currently
# undergoing `tp_clear`.
global func
func = self.ref()
class Cyclic(tuple):
__slots__ = ()
# 4. The finalizers of all garbage objects are called. In
# this case this is only us as `func` doesn't have a
# finalizer.
def __del__(self):
# 5. Create a weakref to `func` now. If we had created
# it earlier, it would have been cleared by the
# garbage collector before calling the finalizers.
self[1].ref = weakref.ref(self[0])
# 6. Drop the global reference to `latefin`. The only
# remaining reference is the one we have.
global latefin
del latefin
# 7. Now `func` is `tp_clear`-ed. This drops the last
# reference to `Cyclic`, which gets `tp_dealloc`-ed.
# This drops the last reference to `latefin`.
latefin = LateFin()
def func():
pass
cyc = tuple.__new__(Cyclic, (func, latefin))
# 1. Create a reference cycle of `cyc` and `func`.
func.__module__ = cyc
# 2. Make the cycle unreachable, but keep the global reference
# to `latefin` so that it isn't detected as garbage. This
# way its finalizer will not be called immediately.
del func, cyc
# 3. Invoke garbage collection,
# which will find `cyc` and `func` as garbage.
gc.collect()
# 9. Previously, this would crash because `func_qualname`
# had been NULL-ed out by func_clear().
print(f"{func=}")
"""
# We're mostly just checking that this doesn't crash.
rc, stdout, stderr = assert_python_ok("-c", code)
self.assertEqual(rc, 0)
self.assertRegex(stdout, rb"""\A\s*func=<function at \S+>\s*\Z""")
self.assertFalse(stderr)

@refcount_test
def test_frame(self):
def f():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed a crash in a garbage-collection edge-case, in which a ``PyFunction_Type.tp_clear`` function could leave a python function object in an inconsistent state.
14 changes: 11 additions & 3 deletions Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -678,18 +678,22 @@ static int
func_clear(PyFunctionObject *op)
{
op->func_version = 0;
Py_CLEAR(op->func_code);
Py_CLEAR(op->func_globals);
Py_CLEAR(op->func_builtins);
Py_CLEAR(op->func_name);
Py_CLEAR(op->func_qualname);
Py_CLEAR(op->func_module);
Py_CLEAR(op->func_defaults);
Py_CLEAR(op->func_kwdefaults);
Py_CLEAR(op->func_doc);
Py_CLEAR(op->func_dict);
Py_CLEAR(op->func_closure);
Py_CLEAR(op->func_annotations);
// Don't Py_CLEAR(op->func_code), since code is always required
// to be non-NULL. Similarly, name and qualname shouldn't be NULL.
// However, name and qualname could be str subclasses, so they
// could have reference cycles. The solution is to replace them
// with a genuinely immutable string.
Py_SETREF(op->func_name, Py_NewRef(&_Py_STR(empty)));
Py_SETREF(op->func_qualname, Py_NewRef(&_Py_STR(empty)));
return 0;
}

Expand All @@ -701,6 +705,10 @@ func_dealloc(PyFunctionObject *op)
PyObject_ClearWeakRefs((PyObject *) op);
}
(void)func_clear(op);
// These aren't cleared by func_clear().
Py_DECREF(op->func_code);
Py_DECREF(op->func_name);
Py_DECREF(op->func_qualname);
PyObject_GC_Del(op);
}

Expand Down

0 comments on commit f2b4e45

Please sign in to comment.