Skip to content

Commit

Permalink
update reference pool counts at the end of allow_threads to avoid use…
Browse files Browse the repository at this point in the history
…-after-free
  • Loading branch information
OliverBalfour authored and davidhewitt committed Feb 21, 2023
1 parent 03ecb80 commit 83f5fa2
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 10 deletions.
1 change: 1 addition & 0 deletions newsfragments/2952.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a reference counting race condition affecting `PyObject`s cloned in `allow_threads` blocks.
32 changes: 22 additions & 10 deletions src/gil.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ impl Drop for GILGuard {
type PyObjVec = Vec<NonNull<ffi::PyObject>>;

/// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held.
struct ReferencePool {
pub(crate) struct ReferencePool {
dirty: atomic::AtomicBool,
// .0 is INCREFs, .1 is DECREFs
pointer_ops: Mutex<(PyObjVec, PyObjVec)>,
Expand All @@ -301,7 +301,7 @@ impl ReferencePool {
self.dirty.store(true, atomic::Ordering::Release);
}

fn update_counts(&self, _py: Python<'_>) {
pub(crate) fn update_counts(&self, _py: Python<'_>) {
let prev = self.dirty.swap(false, atomic::Ordering::Acquire);
if !prev {
return;
Expand All @@ -325,7 +325,7 @@ impl ReferencePool {

unsafe impl Sync for ReferencePool {}

static POOL: ReferencePool = ReferencePool::new();
pub(crate) static POOL: ReferencePool = ReferencePool::new();

/// A RAII pool which PyO3 uses to store owned Python references.
///
Expand Down Expand Up @@ -632,7 +632,9 @@ mod tests {
// Next time the GIL is acquired, the reference is released
Python::with_gil(|py| {
assert_eq!(obj.get_refcnt(py), 1);
assert!(pool_not_dirty());
let non_null = unsafe { NonNull::new_unchecked(obj.as_ptr()) };
assert!(!POOL.pointer_ops.lock().0.contains(&non_null));
assert!(!POOL.pointer_ops.lock().1.contains(&non_null));
});
}

Expand Down Expand Up @@ -693,6 +695,22 @@ mod tests {
assert!(gil_is_acquired());
}

#[test]
fn test_allow_threads_updates_refcounts() {
Python::with_gil(|py| {
// Make a simple object with 1 reference
let obj = get_object(py);
assert!(obj.get_refcnt(py) == 1);
// Clone the object without the GIL to use internal tracking
let escaped_ref = py.allow_threads(|| obj.clone());
// But after the block the refcounts are updated
assert!(obj.get_refcnt(py) == 2);
drop(escaped_ref);
assert!(obj.get_refcnt(py) == 1);
drop(obj);
});
}

#[test]
fn dropping_gil_does_not_invalidate_references() {
// Acquiring GIL for the second time should be safe - see #864
Expand Down Expand Up @@ -800,12 +818,6 @@ mod tests {

REFCNT_CHECKED.wait();

// Returning from allow_threads doesn't clear the pool
py.allow_threads(|| {
// Acquiring GIL will clear the pending change
Python::with_gil(|_| {});
});

println!("6. The main thread has acquired the GIL again and processed the pool.");

// Total reference count should be one higher
Expand Down
2 changes: 2 additions & 0 deletions src/marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ impl<'py> Python<'py> {
gil::GIL_COUNT.with(|c| c.set(self.count));
unsafe {
ffi::PyEval_RestoreThread(self.tstate);
// Update counts of PyObjects / Py that were cloned or dropped during `f`.
gil::POOL.update_counts(Python::assume_gil_acquired());
}
}
}
Expand Down

0 comments on commit 83f5fa2

Please sign in to comment.