Skip to content

Commit 62604ff

Browse files
DinoVmeta-codesync[bot]
authored andcommitted
Generator ref leak fixes
Summary: Re-land of D91839270 There's some issues w/ lightweight frames, generators, as well as generators GC behaviors. We are potentially over visiting things in traverse if we don't have a yield point. We are also under clearing things because we have no clear method. For lightweight frames we need to visit the function object stored before the `_PyInterpreterFrame` on 3.12. But we also need to be careful in how we clear it out in various code paths. The most evil of these is that somehow can call `.clear()` on the reified frame object. This actually doesn't call into *any* of our generator logic - in fact it resumes the generator in the interpreter all on its own! When we deopt it later we need to see it's in a cleared state and decref the function object without transferring it into the JIT generator frame. Reviewed By: alexmalyshev Differential Revision: D93809246 fbshipit-source-id: d6b8a752abdff619cd2d3b7e2e93f42977edbcbe
1 parent e68f537 commit 62604ff

File tree

3 files changed

+87
-23
lines changed

3 files changed

+87
-23
lines changed

cinderx/Jit/frame.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,10 @@ void jitFrameRemoveReifier(_PyInterpreterFrame* frame) {
432432
// interpreter.
433433
if (!hasRtfsFunction(frame)) {
434434
// ownership is transferred
435-
setFrameFunction(frame, jitFrameGetFunction(frame));
435+
if (jitFrameGetFunction(frame) != nullptr) {
436+
setFrameFunction(frame, jitFrameGetFunction(frame));
437+
jitFrameGetHeader(frame)->rtfs = JIT_FRAME_INITIALIZED;
438+
}
436439
} else {
437440
RuntimeFrameState* rtfs = jitFrameGetRtfs(frame);
438441
auto func = rtfs->func();
@@ -634,11 +637,16 @@ void jitFrameClearExceptCode(_PyInterpreterFrame* frame) {
634637
Ci_STACK_CLEAR(frame->localsplus[i]);
635638
}
636639
Ci_STACK_CLOSE(frame->f_funcobj);
637-
if constexpr (PY_VERSION_HEX < 0x030E0000) {
638-
if (!hasRtfsFunction(frame)) {
639-
Py_DECREF(jitFrameGetFunction(frame));
640-
}
640+
#if PY_VERSION_HEX < 0x030E0000
641+
// We can't leave our reifier dangling here otherwise we may
642+
// continue to get callbacks, instead leave the function dangling.
643+
frame->f_funcobj = hasRtfsFunction(frame) ? jitFrameGetRtfs(frame)->func()
644+
: jitFrameGetFunction(frame);
645+
if (!hasRtfsFunction(frame)) {
646+
Py_XDECREF(jitFrameGetFunction(frame));
647+
jitFrameGetHeader(frame)->rtfs = JIT_FRAME_INITIALIZED;
641648
}
649+
#endif
642650
}
643651

644652
RuntimeFrameState runtimeFrameStateFromThreadState(PyThreadState* tstate) {

cinderx/Jit/generators_rt.cpp

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,25 +50,42 @@ int jitgen_traverse(PyObject* obj, visitproc visit, void* arg) {
5050
JitGenObject* jit_gen = JitGenObject::cast(obj);
5151
if (jit_gen != nullptr) {
5252
const GenDataFooter* gen_footer = jit_gen->genDataFooter();
53-
if (gen_footer->yieldPoint == nullptr) {
54-
return 0;
55-
}
56-
size_t deopt_idx = gen_footer->yieldPoint->deoptIdx();
57-
const DeoptMetadata& meta =
58-
gen_footer->code_rt->getDeoptMetadata(deopt_idx);
59-
for (const LiveValue& value : meta.live_values) {
60-
if (value.ref_kind != hir::RefKind::kOwned) {
61-
continue;
53+
// Only visit JIT-specific live values if we have a valid yield point.
54+
// If yieldPoint is null, the generator hasn't yielded yet or has completed,
55+
// but we still need to call PyGen_Type.tp_traverse below to visit standard
56+
// generator references (gi_code, gi_frame, etc.).
57+
if (gen_footer->yieldPoint != nullptr) {
58+
size_t deopt_idx = gen_footer->yieldPoint->deoptIdx();
59+
const DeoptMetadata& meta =
60+
gen_footer->code_rt->getDeoptMetadata(deopt_idx);
61+
for (const LiveValue& value : meta.live_values) {
62+
if (value.ref_kind != hir::RefKind::kOwned) {
63+
continue;
64+
}
65+
codegen::PhyLocation loc = value.location;
66+
JIT_CHECK(
67+
!loc.is_register(),
68+
"DeoptMetadata for Yields should not reference registers");
69+
PyObject* v = *reinterpret_cast<PyObject**>(
70+
reinterpret_cast<uintptr_t>(gen_footer) + loc.loc);
71+
Py_VISIT(v);
6272
}
63-
codegen::PhyLocation loc = value.location;
64-
JIT_CHECK(
65-
!loc.is_register(),
66-
"DeoptMetadata for Yields should not reference registers");
67-
PyObject* v = *reinterpret_cast<PyObject**>(
68-
reinterpret_cast<uintptr_t>(gen_footer) + loc.loc);
69-
Py_VISIT(v);
73+
JIT_CHECK(JitGen_CheckAny(obj), "Deopted during GC traversal");
7074
}
71-
JIT_CHECK(JitGen_CheckAny(obj), "Deopted during GC traversal");
75+
76+
#if PY_VERSION_HEX < 0x030E0000
77+
// In lightweight frame mode, frame->f_funcobj is set to a reifier singleton
78+
// rather than the actual function. The real function is stored in the
79+
// FrameHeader and contains func_closure with closure cells that may
80+
// participate in reference cycles. We must visit it explicitly since
81+
// _PyFrame_Traverse won't see it.
82+
if (getConfig().frame_mode == FrameMode::kLightweight &&
83+
jit_gen->gi_frame_state < FRAME_CLEARED) {
84+
_PyInterpreterFrame* frame = generatorFrame(jit_gen);
85+
BorrowedRef<PyFunctionObject> func = jitFrameGetFunction(frame);
86+
Py_VISIT(func.get());
87+
}
88+
#endif
7289
}
7390
// Try to use CPython traverse as much as we can as it has internals which
7491
// are hard to borrow in 3.14 (compares 'visit' to a speicifc internal
@@ -337,6 +354,26 @@ void jitgen_finalize(PyObject* obj) {
337354
PyGen_Type.tp_finalize(obj);
338355
}
339356

357+
int jitgen_clear(PyObject* obj) {
358+
// tp_clear is called by the cyclic GC to break reference cycles.
359+
// We need to deopt the JIT generator so that the standard generator
360+
// tp_clear can properly clear all references.
361+
//
362+
// If the generator is currently executing (gi_frame_state ==
363+
// FRAME_EXECUTING), we cannot deopt it. In that case, return 0 without
364+
// clearing - the GC will try again later or the generator will complete and
365+
// be collected normally.
366+
if (!deopt_jit_gen(obj)) {
367+
return 0;
368+
}
369+
// After deopting, the object is now a regular PyGenObject/PyCoroObject.
370+
// Delegate to the base type's tp_clear to break cycles.
371+
if (PyGen_Type.tp_clear != nullptr) {
372+
return PyGen_Type.tp_clear(obj);
373+
}
374+
return 0;
375+
}
376+
340377
typedef struct {
341378
PyObject_HEAD
342379
PyObject* cw_coroutine;
@@ -600,6 +637,7 @@ static PyAsyncMethods jitcoro_as_async = {
600637
PyType_Slot gen_slots[] = {
601638
{Py_tp_dealloc, reinterpret_cast<void*>(jitgen_dealloc)},
602639
{Py_tp_traverse, reinterpret_cast<void*>(jitgen_traverse)},
640+
{Py_tp_clear, reinterpret_cast<void*>(jitgen_clear)},
603641
{Py_tp_finalize, reinterpret_cast<void*>(jitgen_finalize)},
604642
{Py_tp_iter, reinterpret_cast<void*>(PyObject_SelfIter)},
605643
{Py_tp_iternext, reinterpret_cast<void*>(jitgen_iternext)},
@@ -635,6 +673,7 @@ static_assert(sizeof(PyGenObject) == sizeof(PyCoroObject));
635673
PyType_Slot coro_slots[] = {
636674
{Py_tp_dealloc, reinterpret_cast<void*>(jitgen_dealloc)},
637675
{Py_tp_traverse, reinterpret_cast<void*>(jitgen_traverse)},
676+
{Py_tp_clear, reinterpret_cast<void*>(jitgen_clear)},
638677
{Py_tp_finalize, reinterpret_cast<void*>(jitgen_finalize)},
639678
{Py_tp_methods, jitcoro_methods},
640679
{Py_tp_members, jitcoro_memberlist},
@@ -666,7 +705,16 @@ void deopt_jit_gen_object_only(JitGenObject* gen) {
666705
Py_SET_TYPE(reinterpret_cast<PyObject*>(gen), type);
667706
if (getConfig().frame_mode == FrameMode::kLightweight) {
668707
auto frame = generatorFrame(gen);
669-
jitFrameRemoveReifier(frame);
708+
if (gen->gi_frame_state != FRAME_CLEARED) {
709+
jitFrameRemoveReifier(frame);
710+
} else if constexpr (PY_VERSION_HEX < 0x030E0000) {
711+
// Normally we'll clear the function via jitFrameClearExceptCode. But
712+
// a user can call clear on a reified frame object which transfers
713+
// ownership of the _PyInterpreterFrame to the PyFrameObject and marks
714+
// the generator frame as cleared. In that case we still need to decref
715+
// the function which is stored before the _PyInterpreterFrame in 3.12.
716+
Py_XDECREF(jitFrameGetFunction(frame));
717+
}
670718
}
671719
}
672720

cinderx/Jit/jit_rt.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,10 +677,18 @@ void JITRT_InitFrameCellVars(
677677
PyCodeObject* co = (PyCodeObject*)func->func_code;
678678
int offset = co->co_nlocalsplus - nvars;
679679
_PyInterpreterFrame* frame = interpFrameFromThreadState(tstate);
680+
for (int i = 0; i < offset; i++) {
681+
frame->localsplus[i] = Ci_STACK_NULL;
682+
}
680683
for (int i = 0; i < nvars; i++) {
681684
frame->localsplus[offset + i] =
682685
Ci_STACK_NEWREF(PyTuple_GET_ITEM(closure, i));
683686
}
687+
#if PY_VERSION_HEX < 0x030E0000
688+
frame->stacktop = nvars + offset;
689+
#else
690+
frame->stackpointer = &frame->localsplus[nvars + offset];
691+
#endif
684692
}
685693

686694
std::pair<PyThreadState*, jit::GenDataFooter*>

0 commit comments

Comments
 (0)