[Python-checkins] gh-91636: Don't clear required fields of function objects (GH-91651)
sweeneyde
webhook-mailer at python.org
Thu Apr 21 02:06:39 EDT 2022
https://github.com/python/cpython/commit/f2b4e458b3327130e46edb4efe8e1847de09efc5
commit: f2b4e458b3327130e46edb4efe8e1847de09efc5
branch: main
author: Dennis Sweeney <36520290+sweeneyde at users.noreply.github.com>
committer: sweeneyde <36520290+sweeneyde at users.noreply.github.com>
date: 2022-04-21T02:06:35-04:00
summary:
gh-91636: Don't clear required fields of function objects (GH-91651)
files:
A Misc/NEWS.d/next/Core and Builtins/2022-04-18-02-45-40.gh-issue-91636.6DFdy_.rst
M Lib/test/test_gc.py
M Objects/funcobject.c
diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index ce04042679bbc..dbbd67b4fc88a 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -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():
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-18-02-45-40.gh-issue-91636.6DFdy_.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-18-02-45-40.gh-issue-91636.6DFdy_.rst
new file mode 100644
index 0000000000000..663339bafb79e
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-04-18-02-45-40.gh-issue-91636.6DFdy_.rst
@@ -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.
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index deacfd55dd286..1e0cfb7efb479 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -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);
}
More information about the Python-checkins
mailing list