Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in freethreading when acquiring/releasing the GIL in a finalizer #119585

Closed
da-woods opened this issue May 26, 2024 · 2 comments
Closed

Crash in freethreading when acquiring/releasing the GIL in a finalizer #119585

da-woods opened this issue May 26, 2024 · 2 comments
Assignees
Labels
topic-free-threading type-bug An unexpected behavior, bug, or error

Comments

@da-woods
Copy link
Contributor

da-woods commented May 26, 2024

Bug report

Bug description:

bug.cpp

#include <thread>
#include <cstdio>
#include <latch>
#include <Python.h>

PyObject* callInDestructor(PyObject *self, PyObject *args) {
    auto state = PyGILState_Ensure();
    printf("In destructor\n");
    PyGILState_Release(state);
    Py_RETURN_NONE;
}

PyObject *doStuff(PyObject *self, PyObject *cls) {
    PyObject *o;
    Py_BEGIN_ALLOW_THREADS  // I'm not really sure why this is needed...
    {
        std::latch l1(1);
        std::latch l2(1);
        std::latch l3(1);
        auto thread1 = std::jthread([&](){
            l1.wait();
            auto state = PyGILState_Ensure();
            o = PyObject_CallNoArgs(cls);
            l2.count_down();
            // printf("0\n");
            l3.wait();
            PyGILState_Release(state);
            printf("thread1 end\n");
        });
        auto thread2 = std::jthread([&](){
            l1.count_down();
            auto state = PyGILState_Ensure();
            l2.wait();
            Py_XDECREF(o);
            l3.count_down();
            PyGILState_Release(state);
            printf("thread2 end\n");
        });
    }
    Py_END_ALLOW_THREADS
    Py_RETURN_NONE;
}

static PyMethodDef methods[] = {
    {"doStuff",  doStuff, METH_O, "Demonstrate error."},
    {"callInDestructor", callInDestructor, METH_NOARGS, "destruct"},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef moduleDef = {
    PyModuleDef_HEAD_INIT,
    "bug",   /* name of module */
    NULL,
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    methods
};

PyMODINIT_FUNC
PyInit_bug(void)
{
    PyObject *m;

    m = PyModule_Create(&moduleDef);
    if (m == NULL)
        return NULL;

    return m;
}

setupbug.py

from setuptools import Extension, setup

setup(
    ext_modules=[
        Extension(
            name="bug",
            sources=["bug.cpp"],
            language="C++",
            extra_compile_args=['-std=c++20'],
            extra_link_args=['-lstdc++'],
        ),
    ]
)

testbug.py

import bug

class C:
    def __del__(self):
        bug.callInDestructor()

bug.doStuff(C)

Build with python setupbug.py build_ext --inplace

Run with python -Xgil=0 testbug.py

I'm a little unclear on exactly what's going wrong here, but essentially it's destroying the C object from within merge_queued_objects(&brc->local_objects_to_merge);, callInDestruction() calls PyGILState_Ensure and PyGILState_Release (which is unnecessary because it already has the GIL, but should be fine anyway), and it's around here that it crashes with a segmentation fault.

If I remove the GIL handing from callInDestruction then it doesn't crash for me.

This is a cut-down version of something I've seen in Cython cython/cython#6214 (comment) with a very similar crash but happening in a slightly different way.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Linked PRs

@da-woods da-woods added the type-bug An unexpected behavior, bug, or error label May 26, 2024
@da-woods da-woods changed the title Crash in freethreading when acquiring/releasing the GIL in a destructor Crash in freethreading when acquiring/releasing the GIL in a finalizer May 26, 2024
@colesbury colesbury self-assigned this May 29, 2024
@colesbury
Copy link
Contributor

Thanks -- I was able to reproduce the issue easily with your repro. The problem is roughly:

  • PyGILState_Release() calls PyThreadState_Clear()
  • PyThreadState_Clear() calls some destructors
  • The destructors call PyGILState_Ensure() and PyGILState_Release(), but the original PyGILState_Release() is started, but not quite finished so it leads to an invalid state.

PyThreadState_Clear() can call destructors in the default build too, but it happens more often in the free-threaded build.

I think the fix is relatively simple: don't decrement the gilstate_counter until after we call PyThreadState_Clear():

--tstate->gilstate_counter;

colesbury added a commit to colesbury/cpython that referenced this issue May 29, 2024
…readState_Clear()`

Don't decrement `gilstate_counter` in `PyGILState_Release()` until after
`PyThreadState_Clear()` is called. A destructor called from
`PyThreadState_Clear()` may call back into `PyGILState_Ensure()` and
PyGILState_Release()` and if `gilstate_counter` is zero, it will try to
create a new thread state before the current active thread state is
destroyed.
colesbury added a commit that referenced this issue May 31, 2024
…ate_Clear()` (#119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
miss-islington pushed a commit to miss-islington/cpython that referenced this issue May 31, 2024
…readState_Clear()` (pythonGH-119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
(cherry picked from commit bcc1be3)

Co-authored-by: Sam Gross <[email protected]>
colesbury added a commit to colesbury/cpython that referenced this issue May 31, 2024
…d `PyThreadState_Clear()` (pythonGH-119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
(cherry picked from commit bcc1be3)

Co-authored-by: Sam Gross <[email protected]>
colesbury added a commit to colesbury/cpython that referenced this issue May 31, 2024
…d `PyThreadState_Clear()` (pythonGH-119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
(cherry picked from commit bcc1be3)

Co-authored-by: Sam Gross <[email protected]>
colesbury added a commit that referenced this issue May 31, 2024
…hreadState_Clear()` (GH-119753) (#119859)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
(cherry picked from commit bcc1be3)

Co-authored-by: Sam Gross <[email protected]>
colesbury added a commit that referenced this issue May 31, 2024
…hreadState_Clear()` (GH-119753) (#119861)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
(cherry picked from commit bcc1be3)
@da-woods
Copy link
Contributor Author

da-woods commented Jun 1, 2024

@colesbury Thanks for the quick PR to fix it.

I'm closing the issue since I'm assuming it was left open as an oversight and not because there's more to do. But please correct that if I'm wrong.

@da-woods da-woods closed this as completed Jun 1, 2024
noahbkim pushed a commit to hudson-trading/cpython that referenced this issue Jul 11, 2024
…readState_Clear()` (python#119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
estyxx pushed a commit to estyxx/cpython that referenced this issue Jul 17, 2024
…readState_Clear()` (python#119753)

Make sure that `gilstate_counter` is not zero in when calling
`PyThreadState_Clear()`. A destructor called from `PyThreadState_Clear()` may
call back into `PyGILState_Ensure()` and `PyGILState_Release()`. If
`gilstate_counter` is zero, it will try to create a new thread state before
the current active thread state is destroyed, leading to an assertion failure
or crash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic-free-threading type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants