pybind / pybind/pybind11

[BUG]: race condition in init with gil_scoped_release causes pybind11_object_dealloc(): Tried to deallocate unregistered instance!

Open
#5,473 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

triage
Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

Required prerequisites
What version (or hash if on master) of pybind11 are you using?

2.13.6, 2.12.1, 741d86f2e3527b667ba85d273a5eea19a0978ef5

Problem description

Hi, I recently found an issue when stress testing pybind related code in PyTorch but have been able to repro it with just pure pybind11 code (https://github.com/d4l3k/pybind11/commit/498874275237351ee178b21171733406081770e1).

The issue seems to be that when using gil_scoped_release in an init() function with multiple threads it causes memory corruption in the object registration and thus crashes when deleting the pybinded object.

terminate called after throwing an instance of 'std::runtime_error'
  what():  pybind11_object_dealloc(): Tried to deallocate unregistered instance!

I suspect what's happening is that the gil_scoped_release is causing the object registration to not be correctly protected resulting in memory corruption. When we then delete it, the entry has been overridden and thus can't be found.

C++ stack trace (originally discovered in PyTorch)

#0  0x00007ffff7c8bacc in __pthread_kill_implementation () from /lib64/libc.so.6
#1  0x00007ffff7c3e686 in raise () from /lib64/libc.so.6
#2  0x00007ffff7c28833 in abort () from /lib64/libc.so.6
#3  0x00007ffff4cd4f00 in __gnu_cxx::__verbose_terminate_handler () at ../../../../libstdc++-v3/libsupc++/vterminate.cc:95
#4  0x00007ffff4cd343c in __cxxabiv1::__terminate (handler=<optimized out>) at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:48
#5  0x00007ffff4cd348e in std::terminate () at ../../../../libstdc++-v3/libsupc++/eh_terminate.cc:58
#6  0x00007ffff4cd3680 in __cxxabiv1::__cxa_throw (obj=0x7fdd2805b700, tinfo=0x7ffff4df0560 <typeinfo for std::runtime_error>, dest=0x7ffff4ce0272 <std::runtime_error::~runtime_error()>)
    at ../../../../libstdc++-v3/libsupc++/eh_throw.cc:98
#7  0x00007fffeec78c82 in pybind11::pybind11_fail(char const*) () from /home/tristanr/.conda/envs/pytorch-3.10/lib/python3.10/site-packages/torch/lib/libtorch_python.so
#8  0x00007fffeeec93f7 in pybind11::detail::clear_instance(_object*) () from /home/tristanr/.conda/envs/pytorch-3.10/lib/python3.10/site-packages/torch/lib/libtorch_python.so
#9  0x00007fffeeec9c71 in pybind11_object_dealloc () from /home/tristanr/.conda/envs/pytorch-3.10/lib/python3.10/site-packages/torch/lib/libtorch_python.so
#10 0x00000000004f10a7 in _Py_DECREF (op=<error reading variable: dwarf2_find_location_expression: Corrupted DWARF expression.>) at /usr/local/src/conda/python-3.10.13/Include/object.h:500

python stack trace (gdb py-bt)

  File "/home/tristanr/pybind11/tests/test_init_race.py", line 17, in run
    del store
  File "/home/tristanr/.conda/envs/pybind-3.10/lib/python3.10/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/tristanr/.conda/envs/pybind-3.10/lib/python3.10/concurrent/futures/thread.py", line 83, in _worker
    work_item.run()
  File "/home/tristanr/.conda/envs/pybind-3.10/lib/python3.10/threading.py", line 953, in run
    self._target(*self._args, **self._kwargs)
  File "/home/tristanr/.conda/envs/pybind-3.10/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
    self.run()
  File "/home/tristanr/.conda/envs/pybind-3.10/lib/python3.10/threading.py", line 973, in _bootstrap
    self._bootstrap_inner()
Reproducible example code

full repro: https://github.com/d4l3k/pybind11/commit/498874275237351ee178b21171733406081770e1

invoked with:

PYTHONPATH=./build/tests/ python tests/test_init_race.py
PYTHONPATH=./build/tests/ gdb -q -ex=r --args (which python) tests/test_init_race.py
// C++
TEST_SUBMODULE(init_race, m) {
    class Simple {};
    py::class_<Simple, std::unique_ptr<Simple>>(m, "Simple")
        .def(py::init([]() {
                 std::this_thread::sleep_for(std::chrono::milliseconds(50));
                 return std::make_unique<Simple>();
             }),
             py::call_guard<py::gil_scoped_release>());
}

Python

import time
from concurrent.futures import ThreadPoolExecutor
import env  # noqa: F401
import pytest
from pybind11_tests import init_race as m

def run():
    store = m.Simple()
    # this sleep is required to trigger the crash
    time.sleep(0.1)
    del store

futures = []
with ThreadPoolExecutor(
    max_workers=100,
) as executor:
    for i in range(100000):
        print(i)
        futures.append(executor.submit(run))
        if len(futures) > 100:
            futures.pop(0).result()

Using Python 3.10 installed via conda.

Is this a regression? Put the last known working version here if it is.

Not a regression

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the reproducer in tests/test_init_race.py and the C++ init_race example in the issue, then run it with PYTHONPATH=./build/tests/ and the provided stress workload. Trace the concurrent gil_scoped_release initialization and object deletion; done means the stress test no longer produces the unregistered-instance crash and has regression coverage.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.