Skip to content

Commit bc3fac9

Browse files
authored
Merge pull request #924 from davidhewitt/fix-deadlock
Fix deadlock in update_counts
2 parents 7e4d1c4 + 465e83b commit bc3fac9

File tree

4 files changed

+63
-60
lines changed

4 files changed

+63
-60
lines changed

.travis.yml

-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,6 @@ env:
3434
- TRAVIS_RUST_VERSION=nightly
3535
- RUST_BACKTRACE=1
3636

37-
addons:
38-
apt:
39-
packages:
40-
- libgoogle-perftools-dev
41-
4237
before_install:
4338
- source ./ci/travis/setup.sh
4439
- curl -L https://github.com/mozilla/grcov/releases/latest/download/grcov-linux-x86_64.tar.bz2 | tar jxf -

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Fixed
9+
- Fix deadlock in `Python::acquire_gil()` after dropping a `PyObject` or `Py<T>`. [#924](https://github.com/PyO3/pyo3/pull/924)
810

9-
## [0.10.0]
11+
## [0.10.0] - 2020-05-13
1012
### Added
1113
- Add FFI definition `_PyDict_NewPresized`. [#849](https://github.com/PyO3/pyo3/pull/849)
1214
- Implement `IntoPy<PyObject>` for `HashSet` and `BTreeSet`. [#864](https://github.com/PyO3/pyo3/pull/864)

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ appveyor = { repository = "fafhrd91/pyo3" }
2222
indoc = { version = "0.3.4", optional = true }
2323
inventory = { version = "0.1.4", optional = true }
2424
libc = "0.2.62"
25+
parking_lot = "0.10.2"
2526
num-bigint = { version = "0.2", optional = true }
2627
num-complex = { version = "0.2", optional = true }
2728
paste = { version = "0.1.6", optional = true }

src/gil.rs

+59-54
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
//! Interaction with python's global interpreter lock
44
55
use crate::{ffi, internal_tricks::Unsendable, Python};
6-
use std::cell::{Cell, RefCell, UnsafeCell};
7-
use std::sync::atomic::{spin_loop_hint, AtomicBool, Ordering};
6+
use parking_lot::{const_mutex, Mutex};
7+
use std::cell::{Cell, RefCell};
88
use std::{any, mem::ManuallyDrop, ptr::NonNull, sync};
99

1010
static START: sync::Once = sync::Once::new();
@@ -168,75 +168,49 @@ impl Drop for GILGuard {
168168

169169
/// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held.
170170
struct ReferencePool {
171-
locked: AtomicBool,
172-
pointers_to_incref: UnsafeCell<Vec<NonNull<ffi::PyObject>>>,
173-
pointers_to_decref: UnsafeCell<Vec<NonNull<ffi::PyObject>>>,
174-
}
175-
176-
struct Lock<'a> {
177-
lock: &'a AtomicBool,
178-
}
179-
180-
impl<'a> Lock<'a> {
181-
fn new(lock: &'a AtomicBool) -> Self {
182-
while lock.compare_and_swap(false, true, Ordering::Acquire) {
183-
spin_loop_hint();
184-
}
185-
Self { lock }
186-
}
187-
}
188-
189-
impl<'a> Drop for Lock<'a> {
190-
fn drop(&mut self) {
191-
self.lock.store(false, Ordering::Release);
192-
}
171+
pointers_to_incref: Mutex<Vec<NonNull<ffi::PyObject>>>,
172+
pointers_to_decref: Mutex<Vec<NonNull<ffi::PyObject>>>,
193173
}
194174

195175
impl ReferencePool {
196176
const fn new() -> Self {
197177
Self {
198-
locked: AtomicBool::new(false),
199-
pointers_to_incref: UnsafeCell::new(Vec::new()),
200-
pointers_to_decref: UnsafeCell::new(Vec::new()),
178+
pointers_to_incref: const_mutex(Vec::new()),
179+
pointers_to_decref: const_mutex(Vec::new()),
201180
}
202181
}
203182

204183
fn register_incref(&self, obj: NonNull<ffi::PyObject>) {
205-
let _lock = Lock::new(&self.locked);
206-
let v = self.pointers_to_incref.get();
207-
unsafe { (*v).push(obj) };
184+
self.pointers_to_incref.lock().push(obj)
208185
}
209186

210187
fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
211-
let _lock = Lock::new(&self.locked);
212-
let v = self.pointers_to_decref.get();
213-
unsafe { (*v).push(obj) };
188+
self.pointers_to_decref.lock().push(obj)
214189
}
215190

216191
fn update_counts(&self, _py: Python) {
217-
let _lock = Lock::new(&self.locked);
192+
macro_rules! swap_vec_with_lock {
193+
// Get vec from one of ReferencePool's mutexes via lock, swap vec if needed, unlock.
194+
($cell:expr) => {{
195+
let mut locked = $cell.lock();
196+
let mut out = Vec::new();
197+
if !locked.is_empty() {
198+
std::mem::swap(&mut out, &mut *locked);
199+
}
200+
drop(locked);
201+
out
202+
}};
203+
};
218204

219205
// Always increase reference counts first - as otherwise objects which have a
220206
// nonzero total reference count might be incorrectly dropped by Python during
221207
// this update.
222-
{
223-
let v = self.pointers_to_incref.get();
224-
unsafe {
225-
for ptr in &(*v) {
226-
ffi::Py_INCREF(ptr.as_ptr());
227-
}
228-
(*v).clear();
229-
}
208+
for ptr in swap_vec_with_lock!(self.pointers_to_incref) {
209+
unsafe { ffi::Py_INCREF(ptr.as_ptr()) };
230210
}
231211

232-
{
233-
let v = self.pointers_to_decref.get();
234-
unsafe {
235-
for ptr in &(*v) {
236-
ffi::Py_DECREF(ptr.as_ptr());
237-
}
238-
(*v).clear();
239-
}
212+
for ptr in swap_vec_with_lock!(self.pointers_to_decref) {
213+
unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
240214
}
241215
}
242216
}
@@ -637,17 +611,48 @@ mod test {
637611

638612
// The pointer should appear once in the incref pool, and once in the
639613
// decref pool (for the clone being created and also dropped)
640-
assert_eq!(unsafe { &*POOL.pointers_to_incref.get() }, &vec![ptr]);
641-
assert_eq!(unsafe { &*POOL.pointers_to_decref.get() }, &vec![ptr]);
614+
assert_eq!(&*POOL.pointers_to_incref.lock(), &vec![ptr]);
615+
assert_eq!(&*POOL.pointers_to_decref.lock(), &vec![ptr]);
642616

643617
// Re-acquring GIL will clear these pending changes
644618
drop(gil);
645619
let _gil = Python::acquire_gil();
646620

647-
assert!(unsafe { (*POOL.pointers_to_incref.get()).is_empty() });
648-
assert!(unsafe { (*POOL.pointers_to_decref.get()).is_empty() });
621+
assert!(POOL.pointers_to_incref.lock().is_empty());
622+
assert!(POOL.pointers_to_decref.lock().is_empty());
649623

650624
// Overall count is still unchanged
651625
assert_eq!(count, obj.get_refcnt());
652626
}
627+
628+
#[test]
629+
fn test_update_counts_does_not_deadlock() {
630+
// update_counts can run arbitrary Python code during Py_DECREF.
631+
// if the locking is implemented incorrectly, it will deadlock.
632+
633+
let gil = Python::acquire_gil();
634+
let obj = get_object(gil.python());
635+
636+
unsafe {
637+
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
638+
// This line will implicitly call update_counts
639+
// -> and so cause deadlock if update_counts is not handling recursion correctly.
640+
let pool = GILPool::new();
641+
642+
// Rebuild obj so that it can be dropped
643+
PyObject::from_owned_ptr(
644+
pool.python(),
645+
ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _,
646+
);
647+
}
648+
649+
let ptr = obj.into_ptr();
650+
let capsule = ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop));
651+
652+
POOL.register_decref(NonNull::new(capsule).unwrap());
653+
654+
// Updating the counts will call decref on the capsule, which calls capsule_drop
655+
POOL.update_counts(gil.python())
656+
}
657+
}
653658
}

0 commit comments

Comments
 (0)