STINNER Victor <vstin...@python.org> added the comment:

Right now, threading_shutdown_interrupted.py does crash on the master branch 
(commit 05e4a296ecc127641160a04f39cc02c0f66a8c27). I reintroduced the bug with:

commit 9ad58acbe8b90b4d0f2d2e139e38bb5aa32b7fb6
Author: Victor Stinner <vstin...@python.org>
Date:   Mon Mar 9 23:37:49 2020 +0100

    bpo-19466: Py_Finalize() clears daemon threads earlier (GH-18848)
    
    Clear the frames of daemon threads earlier during the Python shutdown to
    call objects destructors. So "unclosed file" resource warnings are now
    emitted for daemon threads in a more reliable way.

I investigated this bug with Pablo Galindo Salgado. When the bug triggers, 
there are 2 strong references to a Frame object:

* Traceback object
* Generator object

The bug occurs in "except ValueError as exc:" of EvilThread.coroutine().

The exception contains a traceback object which has a strong reference to the 
frame.

The frame object contains "exc" variable (the exception) which contains a 
reference to the tracecback (exc.__traceback__) which contains a refererence to 
the frame (tb.tb_frame): there is a reference cycle. (It's not really an issue 
by itself).

There are 2 strong references to the Frame object.

Py_FinalizeEx() calls _PyThreadState_DeleteExcept() which doesn't clear the 
reference cycle. But PyThreadState_Clear() calls Py_CLEAR(tstate->frame) which 
reduces the Frame reference counter from 2 to 1, even if there are still 2 
strong references to it (traceback and generator).

tstate->frame is only set at 3 places:

* new_threadstate(): when a Python thread state is created (it's initialized to 
NULL)
* _PyEval_EvalFrameDefault() entry: set to frame
* _PyEval_EvalFrameDefault() exit: set to frame->f_back (which can be NULL

By the way, _PyEval_EvalFrameDefault() has a bug: tstate->frame is not reset if 
_PyCode_InitOpcache() fails: bpo-40048.

Py_FinalizeEx() calls _PyRuntimeState_SetFinalizing(runtime, tstate) and so 
threads which tries to take the GIL must exit immediately. Extract of 
take_gil():
---
static inline int
tstate_must_exit(PyThreadState *tstate)
{
    PyThreadState *finalizing = _PyRuntimeState_GetFinalizing(&_PyRuntime);
    return (finalizing != NULL && finalizing != tstate);
}

static void
take_gil(PyThreadState *tstate)
{
    ...

    if (tstate_must_exit(tstate)) {
        PyThread_exit_thread();
    }
    ...
}
---

The thread is exited in the middle of _PyEval_EvalFrameDefault() by take_gil() 
which calls PyThread_exit_thread() because the thread must exit (because the 
main thread destroyed its Python thread state and so accessing it would crash 
the thread).

_PyThreadState_DeleteExcept() calls PyThreadState_Clear() which calls 
Py_CLEAR(tstate->frame): this case only occurs because the thread exited in the 
middle of _PyEval_EvalFrameDefault(). In the normal case, 
_PyEval_EvalFrameDefault() always resets tstate->frame to its previous value.

Py_CLEAR(tstate->frame) is wrong: it's a *borrowed* reference.

Pablo and me agree with PyThreadState_Clear() must not call 
Py_CLEAR(tstate->frame): it's borrowed reference, not a strong reference.

I'm working on a PR to fix the issue.

----------
resolution: wont fix -> 
status: closed -> open

_______________________________________
Python tracker <rep...@bugs.python.org>
<https://bugs.python.org/issue20526>
_______________________________________
_______________________________________________
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com

Reply via email to