GC in free-threaded build has problems with `Py_INCREF()`/`Py_DECREF()` in `tp_traverse` handlers
@pablogsal 已经在做这个了。
开始于 2024年8月22日。
- 主要语言
- Python
- 星标
- 77.2k
- 派生
- 35.9k
- PR 合并指标
- PR 指标待抓取
描述
Bug report
This came up in the context of nanobind. Nanobind implements (in a test) a traverse function:
int funcwrapper_tp_traverse(PyObject *self, visitproc visit, void *arg) {
FuncWrapper *w = nb::inst_ptr<FuncWrapper>(self);
nb::object f = nb::cast(w->f, nb::rv_policy::none);
Py_VISIT(f.ptr());
return 0;
};
The nb::object smart pointer is internally reference counted. In other words, the above is roughly equivalent to:
int funcwrapper_tp_traverse(PyObject *self, visitproc visit, void *arg) {
PyObject *f = self->w->f;
Py_INCREF(f);
Py_VISIT(f);
Py_DECREF(f);
return 0;
};
This leads to a leak in the free-threaded GC for subtle reasons: when determining resurrected objects, the free-threaded GC uses ob_ref_local to compute the refcount - incoming references, which may be (temporarily) negative. In this case, Py_INCREF() adds 1 to the refcount, but by the time Py_DECREF() is called, the local refcount is -1 which makes the object appear immortal.
There are a number of limitations on the implementations of traverse functions, which are not well documented. For example, it's not safe to allocate, free, track, or untrack Python objects. It's unclear to me whether there are other issues with calling refcounting functions in traverse callbacks.
I think we can make handle_resurrected_objects more robust to this by splitting the first pass over state->unreachable into two passes.
See also: https://github.com/PyO3/pyo3/issues/3165, which was not related to the free-threaded build.
Linked PRs
- gh-142232
- gh-142271
- gh-142272
- gh-142422
- gh-142423
- gh-142567
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
评估
这个 Issue 还没有评估数据。