gh-91636: Don't clear required fields of function objects (GH-91651)

This commit is contained in:
Dennis Sweeney 2022-04-21 02:06:35 -04:00 committed by GitHub
parent 615b24c80b
commit f2b4e458b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 3 deletions

View File

@ -227,6 +227,73 @@ class GCTests(unittest.TestCase):
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():

View File

@ -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.

View File

@ -678,11 +678,8 @@ 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);
@ -690,6 +687,13 @@ func_clear(PyFunctionObject *op)
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;
}
@ -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);
}