[BUG]: race condition in init with gil_scoped_release causes pybind11_object_dealloc(): Tried to deallocate unregistered instance!
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 18k
- Forks
- 2.3k
- Avg merge
- 5d 17h
- Merged PRs (30d)
- 10
Description
Required prerequisites
- Make sure you've read the documentation. Your issue may be addressed there.
- Search the issue tracker and Discussions to verify that this hasn't already been reported. +1 or comment there if it has.
- Consider asking first in the Gitter chat room or in a Discussion.
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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