Skip to content

Commit 8e84721

Browse files
authored
Merge pull request #893 from davidhewitt/safe_acquire_gil
Close soundness hole with acquire_gil
2 parents 818ebf7 + 8ffe8c5 commit 8e84721

File tree

4 files changed

+134
-31
lines changed

4 files changed

+134
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2020
- When the GIL is not held, the refcount is now decreased when the GIL is next acquired. (Previously would wait until next time the GIL was released.)
2121
- `FromPyObject` for `Py<T>` now works for a wider range of `T`, in particular for `T: PyClass`. [#880](https://github.com/PyO3/pyo3/pull/880)
2222
- Some functions such as `PyList::get_item` which return borrowed objects at the C ffi layer now return owned objects at the PyO3 layer, for safety reasons. [#890](https://github.com/PyO3/pyo3/pull/890)
23+
- The `GILGuard` returned from `Python::acquire_gil` will now only assume responsiblity for freeing owned references on drop if no other `GILPool` or `GILGuard` exists. This ensures that multiple calls to the safe api `Python::acquire_gil` cannot lead to dangling references. [#893](https://github.com/PyO3/pyo3/pull/893)
2324
- The trait `ObjectProtocol` has been removed, and all the methods from the trait have been moved to `PyAny`. [#911](https://github.com/PyO3/pyo3/pull/911)
2425
- The exception to this is `ObjectProtocol::None`, which has simply been removed. Use `Python::None` instead.
2526

guide/src/advanced.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ PyO3 exposes much of Python's C API through the `ffi` module.
66

77
The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API.
88

9+
## Memory Management
10+
11+
PyO3's "owned references" (`&PyAny` etc.) make PyO3 more ergonomic to use by ensuring that their lifetime can never be longer than the duration the Python GIL is held. This means that most of PyO3's API can assume the GIL is held. (If PyO3 could not assume this, every PyO3 API would need to take a `Python` GIL token to prove that the GIL is held.)
12+
13+
The caveat to these "owned references" is that Rust references do not normally convey ownership (they are always `Copy`, and cannot implement `Drop`). Whenever a PyO3 API returns an owned reference, PyO3 stores it internally, so that PyO3 can decrease the reference count just before PyO3 releases the GIL.
14+
15+
For most use cases this behaviour is invisible. Occasionally, however, users may need to clear memory usage sooner than PyO3 usually does. PyO3 exposes this functionality with the the `GILPool` struct. When a `GILPool` is dropped, ***all*** owned references created after the `GILPool` was created will be cleared.
16+
17+
The unsafe function `Python::new_pool` allows you to create a new `GILPool`. When doing this, you must be very careful to ensure that once the `GILPool` is dropped you do not retain access any owned references created after the `GILPool` was created.
18+
919
## Testing
1020

1121
Currently, [#341](https://github.com/PyO3/pyo3/issues/341) causes `cargo test` to fail with weird linking errors when the `extension-module` feature is activated. For now you can work around this by making the `extension-module` feature optional and running the tests with `cargo test --no-default-features`:

src/gil.rs

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ static START: sync::Once = sync::Once::new();
1212
thread_local! {
1313
/// This is a internal counter in pyo3 monitoring whether this thread has the GIL.
1414
///
15-
/// It will be incremented whenever a GIL-holding RAII struct is created, and decremented
16-
/// whenever they are dropped.
15+
/// It will be incremented whenever a GILPool is created, and decremented whenever they are
16+
/// dropped.
1717
///
1818
/// As a result, if this thread has the GIL, GIL_COUNT is greater than zero.
1919
///
@@ -115,22 +115,35 @@ pub fn prepare_freethreaded_python() {
115115
#[must_use]
116116
pub struct GILGuard {
117117
gstate: ffi::PyGILState_STATE,
118-
pool: ManuallyDrop<GILPool>,
118+
pool: ManuallyDrop<Option<GILPool>>,
119119
}
120120

121121
impl GILGuard {
122-
/// Acquires the global interpreter lock, which allows access to the Python runtime.
122+
/// Acquires the global interpreter lock, which allows access to the Python runtime. This is
123+
/// safe to call multiple times without causing a deadlock.
123124
///
124125
/// If the Python runtime is not already initialized, this function will initialize it.
125126
/// See [prepare_freethreaded_python()](fn.prepare_freethreaded_python.html) for details.
127+
///
128+
/// If PyO3 does not yet have a `GILPool` for tracking owned PyObject references, then this
129+
/// new `GILGuard` will also contain a `GILPool`.
126130
pub fn acquire() -> GILGuard {
127131
prepare_freethreaded_python();
128132

129133
unsafe {
130134
let gstate = ffi::PyGILState_Ensure(); // acquire GIL
135+
136+
// If there's already a GILPool, we should not create another or this could lead to
137+
// incorrect dangling references in safe code (see #864).
138+
let pool = if !gil_is_acquired() {
139+
Some(GILPool::new())
140+
} else {
141+
None
142+
};
143+
131144
GILGuard {
132145
gstate,
133-
pool: ManuallyDrop::new(GILPool::new()),
146+
pool: ManuallyDrop::new(pool),
134147
}
135148
}
136149
}
@@ -208,7 +221,7 @@ unsafe impl Sync for ReleasePool {}
208221

209222
static POOL: ReleasePool = ReleasePool::new();
210223

211-
#[doc(hidden)]
224+
/// A RAII pool which PyO3 uses to store owned Python references.
212225
pub struct GILPool {
213226
owned_objects_start: usize,
214227
owned_anys_start: usize,
@@ -217,8 +230,13 @@ pub struct GILPool {
217230
}
218231

219232
impl GILPool {
233+
/// Create a new `GILPool`. This function should only ever be called with the GIL.
234+
///
235+
/// It is recommended not to use this API directly, but instead to use `Python::new_pool`, as
236+
/// that guarantees the GIL is held.
237+
///
220238
/// # Safety
221-
/// This function requires that GIL is already acquired.
239+
/// As well as requiring the GIL, see the notes on `Python::new_pool`.
222240
#[inline]
223241
pub unsafe fn new() -> GILPool {
224242
increment_gil_count();
@@ -230,8 +248,10 @@ impl GILPool {
230248
no_send: Unsendable::default(),
231249
}
232250
}
233-
pub unsafe fn python(&self) -> Python {
234-
Python::assume_gil_acquired()
251+
252+
/// Get the Python token associated with this `GILPool`.
253+
pub fn python(&self) -> Python {
254+
unsafe { Python::assume_gil_acquired() }
235255
}
236256
}
237257

@@ -325,13 +345,13 @@ mod test {
325345
use crate::{ffi, gil, AsPyPointer, IntoPyPointer, PyObject, Python, ToPyObject};
326346
use std::ptr::NonNull;
327347

328-
fn get_object() -> PyObject {
329-
// Convenience function for getting a single unique object
330-
let gil = Python::acquire_gil();
331-
let py = gil.python();
348+
fn get_object(py: Python) -> PyObject {
349+
// Convenience function for getting a single unique object, using `new_pool` so as to leave
350+
// the original pool state unchanged.
351+
let pool = unsafe { py.new_pool() };
352+
let py = pool.python();
332353

333354
let obj = py.eval("object()", None, None).unwrap();
334-
335355
obj.to_object(py)
336356
}
337357

@@ -343,21 +363,21 @@ mod test {
343363
fn test_owned() {
344364
let gil = Python::acquire_gil();
345365
let py = gil.python();
346-
let obj = get_object();
366+
let obj = get_object(py);
347367
let obj_ptr = obj.as_ptr();
348368
// Ensure that obj does not get freed
349369
let _ref = obj.clone_ref(py);
350370

351371
unsafe {
352372
{
353-
let gil = Python::acquire_gil();
354-
gil::register_owned(gil.python(), NonNull::new_unchecked(obj.into_ptr()));
373+
let pool = py.new_pool();
374+
gil::register_owned(pool.python(), NonNull::new_unchecked(obj.into_ptr()));
355375

356-
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
357376
assert_eq!(owned_object_count(), 1);
377+
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
358378
}
359379
{
360-
let _gil = Python::acquire_gil();
380+
let _pool = py.new_pool();
361381
assert_eq!(owned_object_count(), 0);
362382
assert_eq!(ffi::Py_REFCNT(obj_ptr), 1);
363383
}
@@ -368,23 +388,23 @@ mod test {
368388
fn test_owned_nested() {
369389
let gil = Python::acquire_gil();
370390
let py = gil.python();
371-
let obj = get_object();
391+
let obj = get_object(py);
372392
// Ensure that obj does not get freed
373393
let _ref = obj.clone_ref(py);
374394
let obj_ptr = obj.as_ptr();
375395

376396
unsafe {
377397
{
378-
let _pool = GILPool::new();
398+
let _pool = py.new_pool();
379399
assert_eq!(owned_object_count(), 0);
380400

381401
gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr()));
382402

383403
assert_eq!(owned_object_count(), 1);
384404
assert_eq!(ffi::Py_REFCNT(obj_ptr), 2);
385405
{
386-
let _pool = GILPool::new();
387-
let obj = get_object();
406+
let _pool = py.new_pool();
407+
let obj = get_object(py);
388408
gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr()));
389409
assert_eq!(owned_object_count(), 2);
390410
}
@@ -401,7 +421,7 @@ mod test {
401421
fn test_pyobject_drop_with_gil_decreases_refcnt() {
402422
let gil = Python::acquire_gil();
403423
let py = gil.python();
404-
let obj = get_object();
424+
let obj = get_object(py);
405425
// Ensure that obj does not get freed
406426
let _ref = obj.clone_ref(py);
407427
let obj_ptr = obj.as_ptr();
@@ -422,7 +442,7 @@ mod test {
422442
fn test_pyobject_drop_without_gil_doesnt_decrease_refcnt() {
423443
let gil = Python::acquire_gil();
424444
let py = gil.python();
425-
let obj = get_object();
445+
let obj = get_object(py);
426446
// Ensure that obj does not get freed
427447
let _ref = obj.clone_ref(py);
428448
let obj_ptr = obj.as_ptr();
@@ -465,15 +485,16 @@ mod test {
465485
drop(pool);
466486
assert_eq!(get_gil_count(), 2);
467487

488+
// Creating a new GILGuard should not increment the gil count if a GILPool already exists
468489
let gil2 = Python::acquire_gil();
469-
assert_eq!(get_gil_count(), 3);
470-
471-
drop(gil2);
472490
assert_eq!(get_gil_count(), 2);
473491

474492
drop(pool2);
475493
assert_eq!(get_gil_count(), 1);
476494

495+
drop(gil2);
496+
assert_eq!(get_gil_count(), 1);
497+
477498
drop(gil);
478499
assert_eq!(get_gil_count(), 0);
479500
}
@@ -482,7 +503,7 @@ mod test {
482503
fn test_allow_threads() {
483504
let gil = Python::acquire_gil();
484505
let py = gil.python();
485-
let object = get_object();
506+
let object = get_object(py);
486507

487508
py.allow_threads(move || {
488509
// Should be no pointers to drop
@@ -499,12 +520,27 @@ mod test {
499520
// (Acquiring the GIL should have cleared the pool).
500521
assert!(unsafe { (*POOL.pointers_to_drop.get()).is_empty() });
501522

502-
let object = get_object();
523+
let object = get_object(gil.python());
503524
drop(object);
504525
drop(gil);
505526

506527
// Previous drop should have decreased count immediately instead of put in pool
507528
assert!(unsafe { (*POOL.pointers_to_drop.get()).is_empty() });
508529
})
509530
}
531+
532+
#[test]
533+
fn dropping_gil_does_not_invalidate_references() {
534+
// Acquiring GIL for the second time should be safe - see #864
535+
let gil = Python::acquire_gil();
536+
let py = gil.python();
537+
let obj;
538+
539+
let gil2 = Python::acquire_gil();
540+
obj = py.eval("object()", None, None).unwrap();
541+
drop(gil2);
542+
543+
// After gil2 drops, obj should still have a reference count of one
544+
assert_eq!(unsafe { ffi::Py_REFCNT(obj.as_ptr()) }, 1);
545+
}
510546
}

src/python.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// based on Daniel Grunwald's https://github.com/dgrunwald/rust-cpython
44

55
use crate::err::{PyDowncastError, PyErr, PyResult};
6-
use crate::gil::{self, GILGuard};
6+
use crate::gil::{self, GILGuard, GILPool};
77
use crate::type_object::{PyTypeInfo, PyTypeObject};
88
use crate::types::{PyAny, PyDict, PyModule, PyType};
99
use crate::{
@@ -292,6 +292,62 @@ impl<'p> Python<'p> {
292292
pub fn NotImplemented(self) -> PyObject {
293293
unsafe { PyObject::from_borrowed_ptr(self, ffi::Py_NotImplemented()) }
294294
}
295+
296+
/// Create a new pool for managing PyO3's owned references.
297+
///
298+
/// When this `GILPool` is dropped, all PyO3 owned references created after this `GILPool` will
299+
/// all have their Python reference counts decremented, potentially allowing Python to drop
300+
/// the corresponding Python objects.
301+
///
302+
/// Typical usage of PyO3 will not need this API, as `Python::acquire_gil` automatically
303+
/// creates a `GILPool` where appropriate.
304+
///
305+
/// Advanced uses of PyO3 which perform long-running tasks which never free the GIL may need
306+
/// to use this API to clear memory, as PyO3 usually does not clear memory until the GIL is
307+
/// released.
308+
///
309+
/// # Example
310+
/// ```rust
311+
/// # use pyo3::prelude::*;
312+
/// let gil = Python::acquire_gil();
313+
/// let py = gil.python();
314+
///
315+
/// // Some long-running process like a webserver, which never releases the GIL.
316+
/// loop {
317+
/// // Create a new pool, so that PyO3 can clear memory at the end of the loop.
318+
/// let pool = unsafe { py.new_pool() };
319+
///
320+
/// // It is recommended to *always* immediately set py to the pool's Python, to help
321+
/// // avoid creating references with invalid lifetimes.
322+
/// let py = unsafe { pool.python() };
323+
///
324+
/// // do stuff...
325+
/// # break; // Exit the loop so that doctest terminates!
326+
/// }
327+
/// ```
328+
///
329+
/// # Safety
330+
/// Extreme care must be taken when using this API, as misuse can lead to accessing invalid
331+
/// memory. In addition, the caller is responsible for guaranteeing that the GIL remains held
332+
/// for the entire lifetime of the returned `GILPool`.
333+
///
334+
/// Two best practices are required when using this API:
335+
/// - From the moment `new_pool()` is called, only the `Python` token from the returned
336+
/// `GILPool` (accessible using `.python()`) should be used in PyO3 APIs. All other older
337+
/// `Python` tokens with longer lifetimes are unsafe to use until the `GILPool` is dropped,
338+
/// because they can be used to create PyO3 owned references which have lifetimes which
339+
/// outlive the `GILPool`.
340+
/// - Similarly, methods on existing owned references will implicitly refer back to the
341+
/// `Python` token which that reference was originally created with. If the returned values
342+
/// from these methods are owned references they will inherit the same lifetime. As a result,
343+
/// Rust's lifetime rules may allow them to outlive the `GILPool`, even though this is not
344+
/// safe for reasons discussed above. Care must be taken to never access these return values
345+
/// after the `GILPool` is dropped, unless they are converted to `Py<T>` *before* the pool
346+
/// is dropped.
347+
#[inline]
348+
pub unsafe fn new_pool(self) -> GILPool {
349+
GILPool::new()
350+
}
295351
}
296352

297353
impl<'p> Python<'p> {

0 commit comments

Comments
 (0)