Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1c12b51

Browse files
committedJan 2, 2024
- Make proptest a conditional dependency
- Add multi-threaded tests for godot-cell - Add blocking drop method for InaccessibleGuard - Fix UB when InaccessibleGuard is blocking dropped in the wrong order in multi-threaded code
1 parent 2245154 commit 1c12b51

File tree

11 files changed

+403
-209
lines changed

11 files changed

+403
-209
lines changed
 

‎.github/workflows/full-ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,20 @@ jobs:
178178
- name: "Test tree borrows"
179179
run: MIRIFLAGS="-Zmiri-tree-borrows" cargo miri test -p godot-cell
180180

181+
proptest:
182+
name: proptest
183+
runs-on: ubuntu-20.04
184+
steps:
185+
- uses: actions/checkout@v4
186+
187+
- name: "Install Rust"
188+
uses: ./.github/composite/rust
189+
190+
- name: "Compile tests"
191+
run: cargo test -p godot-cell --features="proptest" --no-run
192+
193+
- name: "Test"
194+
run: cargo test -p godot-cell --features="proptest"
181195

182196
# For complex matrix workflow, see https://stackoverflow.com/a/65434401
183197
godot-itest:

‎godot-cell/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ name = "godot-cell"
33
version = "0.1.0"
44
edition = "2021"
55

6-
[dev-dependencies]
7-
proptest = "1.4.0"
6+
[features]
7+
proptest = ["dep:proptest"]
8+
9+
[dependencies]
10+
proptest = { version = "1.4.0", optional = true }

‎godot-cell/src/borrow_state.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl BorrowState {
7373
/// Set self as having reached an erroneous or unreliable state.
7474
///
7575
/// Always returns [`BorrowStateErr::Poisoned`].
76-
fn poison(&mut self, err: impl Into<String>) -> Result<(), BorrowStateErr> {
76+
pub(crate) fn poison(&mut self, err: impl Into<String>) -> Result<(), BorrowStateErr> {
7777
self.poisoned = true;
7878

7979
Err(BorrowStateErr::Poisoned(err.into()))
@@ -230,6 +230,10 @@ impl BorrowState {
230230
Ok(self.inaccessible_count)
231231
}
232232

233+
pub(crate) fn may_unset_inaccessible(&self) -> bool {
234+
!self.has_accessible() && self.shared_count() == 0 && self.inaccessible_count > 0
235+
}
236+
233237
pub fn unset_inaccessible(&mut self) -> Result<usize, BorrowStateErr> {
234238
if self.has_accessible() {
235239
return Err("cannot set current reference as accessible when an accessible mutable reference already exists".into());
@@ -294,7 +298,7 @@ impl From<String> for BorrowStateErr {
294298
}
295299
}
296300

297-
#[cfg(all(test, not(miri)))]
301+
#[cfg(all(test, feature = "proptest"))]
298302
mod proptests {
299303
use super::*;
300304
use proptest::{arbitrary::Arbitrary, collection::vec, prelude::*};

‎godot-cell/src/guards.rs

Lines changed: 91 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use std::ops::{Deref, DerefMut};
99
use std::ptr::NonNull;
10-
use std::sync::Mutex;
10+
use std::sync::{Mutex, MutexGuard};
1111

1212
use crate::CellState;
1313

@@ -69,8 +69,9 @@ impl<'a, T> Drop for RefGuard<'a, T> {
6969

7070
/// Wraps a mutably borrowed value of type `T`.
7171
///
72-
/// This prevents all other borrows of `value`, unless the `&mut` reference handed out from this guard is
73-
/// made inaccessible by a call to [`GdCell::make_inaccessible()`](crate::GdCell::make_inaccessible).
72+
/// This prevents all other borrows of `value` while this guard is accessible. To make this guard
73+
/// inaccessible, use [`GdCell::make_inaccessible()`](crate::GdCell::make_inaccessible) on a mutable
74+
/// reference handed out by this guard.
7475
#[derive(Debug)]
7576
pub struct MutGuard<'a, T> {
7677
state: &'a Mutex<CellState<T>>,
@@ -101,11 +102,11 @@ impl<'a, T> MutGuard<'a, T> {
101102
/// This is the case because:
102103
/// - [`GdCell`](super::GdCell) will not create any new references while this guard exists and is
103104
/// accessible.
104-
/// - When it is marked as inaccessible it is impossible to have any `&self` or `&mut self` references to
105-
/// this guard that can be used. Because we take in a `&mut self` reference with a lifetime `'a` and
106-
/// return an [`InaccessibleGuard`] with a lifetime `'b` where `'a: 'b` which ensure that the
107-
/// `&mut self` outlives that guard and cannot be used until the guard is dropped. And the rust
108-
/// borrow-checker will prevent any new references from being made.
105+
/// - When it is made inaccessible it is impossible to have any `&self` or `&mut self` references to this
106+
/// guard that can be used. Because we take in a `&mut self` reference with a lifetime `'a` and return
107+
/// an [`InaccessibleGuard`] with a lifetime `'b` where `'a: 'b` which ensure that the `&mut self`
108+
/// outlives that guard and cannot be used until the guard is dropped. And the rust borrow-checker will
109+
/// prevent any new references from being made.
109110
/// - When it is made inaccessible, [`GdCell`](super::GdCell) will also ensure that any new references
110111
/// are derived from this guard's `value` pointer, thus preventing `value` from being invalidated.
111112
pub(crate) unsafe fn new(
@@ -128,13 +129,23 @@ impl<'a, T> Deref for MutGuard<'a, T> {
128129
let count = self.state.lock().unwrap().borrow_state.mut_count();
129130
// This is just a best-effort error check. It should never be triggered.
130131
assert_eq!(
131-
self.count, count,
132-
"attempted to access the non-current mutable borrow. **this is a bug, please report it**"
132+
self.count,
133+
count,
134+
"\
135+
attempted to access a non-current mutable borrow of type: `{}`. \n\
136+
current count: {}\n\
137+
value pointer: {:p}\n\
138+
attempted access count: {}\n\
139+
**this is a bug, please report it**\
140+
",
141+
std::any::type_name::<T>(),
142+
self.count,
143+
self.value,
144+
count
133145
);
134146

135-
// SAFETY:
136-
// It is safe to call `as_ref()` on value when we have a `&self` reference because of the safety
137-
// invariants of `new`.
147+
// SAFETY: It is safe to call `as_ref()` on value when we have a `&self` reference because of the
148+
// safety invariants of `new`.
138149
unsafe { self.value.as_ref() }
139150
}
140151
}
@@ -144,8 +155,19 @@ impl<'a, T> DerefMut for MutGuard<'a, T> {
144155
let count = self.state.lock().unwrap().borrow_state.mut_count();
145156
// This is just a best-effort error check. It should never be triggered.
146157
assert_eq!(
147-
self.count, count,
148-
"attempted to access the non-current mutable borrow. **this is a bug, please report it**"
158+
self.count,
159+
count,
160+
"\
161+
attempted to access a non-current mutable borrow of type: `{}`. \n\
162+
current count: {}\n\
163+
value pointer: {:p}\n\
164+
attempted access count: {}\n\
165+
**this is a bug, please report it**\
166+
",
167+
std::any::type_name::<T>(),
168+
self.count,
169+
self.value,
170+
count
149171
);
150172

151173
// SAFETY:
@@ -170,22 +192,29 @@ impl<'a, T> Drop for MutGuard<'a, T> {
170192

171193
/// A guard that ensures a mutable reference is kept inaccessible until it is dropped.
172194
///
173-
/// The current reference is stored in the guard and we push a new reference to `state` on creation. We then
174-
/// undo this upon dropping the guard.
195+
/// We store the current reference in the guard upon creation, and push a new reference to `state` on
196+
/// creation. When the guard is dropped, `state`'s pointer is reset to the original pointer.
175197
///
176-
/// This ensure that any new references are derived from the new reference we pass in, and when this guard is
177-
/// dropped we reset it to the previous reference.
198+
/// This ensures that any new references are derived from the new reference we pass in, and when this guard
199+
/// is dropped the state is reset to what it was before, as if this guard never existed.
178200
#[derive(Debug)]
179201
pub struct InaccessibleGuard<'a, T> {
180202
state: &'a Mutex<CellState<T>>,
203+
stack_depth: usize,
181204
prev_ptr: NonNull<T>,
182205
}
183206

184207
impl<'a, T> InaccessibleGuard<'a, T> {
185208
/// Create a new inaccessible guard for `state`.
186209
///
187210
/// Since `'b` must outlive `'a`, we cannot have any other references aliasing `new_ref` while this
188-
/// guard exists.
211+
/// guard exists. So this guard ensures that the guard that handed out `new_ref` is inaccessible while
212+
/// this guard exists.
213+
///
214+
/// Will error if:
215+
/// - There is currently no accessible mutable borrow.
216+
/// - There are any shared references.
217+
/// - `new_ref` is not equal to the pointer in `state`.
189218
pub(crate) fn new<'b>(
190219
state: &'a Mutex<CellState<T>>,
191220
new_ref: &'b mut T,
@@ -205,16 +234,53 @@ impl<'a, T> InaccessibleGuard<'a, T> {
205234

206235
guard.borrow_state.set_inaccessible()?;
207236
let prev_ptr = guard.get_ptr();
208-
guard.set_ptr(new_ptr);
237+
let stack_depth = guard.push_ptr(new_ptr);
238+
239+
Ok(Self {
240+
state,
241+
stack_depth,
242+
prev_ptr,
243+
})
244+
}
209245

210-
Ok(Self { state, prev_ptr })
246+
/// Single implementation of drop-logic for use in both drop implementations.
247+
fn perform_drop(
248+
mut state: MutexGuard<'_, CellState<T>>,
249+
prev_ptr: NonNull<T>,
250+
stack_depth: usize,
251+
) {
252+
if state.stack_depth != stack_depth {
253+
state
254+
.borrow_state
255+
.poison("cannot drop inaccessible guards in the wrong order")
256+
.unwrap();
257+
}
258+
state.borrow_state.unset_inaccessible().unwrap();
259+
state.pop_ptr(prev_ptr);
260+
}
261+
262+
/// Drop self if possible, otherwise returns self again.
263+
///
264+
/// Used currently in the mock-tests, as we need a thread safe way to drop self. Using the normal drop
265+
/// logic may poison state, however it should not cause any UB either way.
266+
#[doc(hidden)]
267+
pub fn try_drop(self) -> Result<(), std::mem::ManuallyDrop<Self>> {
268+
let manual = std::mem::ManuallyDrop::new(self);
269+
let state = manual.state.lock().unwrap();
270+
if !state.borrow_state.may_unset_inaccessible() || state.stack_depth != manual.stack_depth {
271+
return Err(manual);
272+
}
273+
Self::perform_drop(state, manual.prev_ptr, manual.stack_depth);
274+
275+
Ok(())
211276
}
212277
}
213278

214279
impl<'a, T> Drop for InaccessibleGuard<'a, T> {
215280
fn drop(&mut self) {
216-
let mut state = self.state.lock().unwrap();
217-
state.borrow_state.unset_inaccessible().unwrap();
218-
state.set_ptr(self.prev_ptr);
281+
// Default behavior of drop-logic simply panics and poisons the cell on failure. This is appropriate
282+
// for single-threaded code where no errors should happen here.
283+
let state = self.state.lock().unwrap();
284+
Self::perform_drop(state, self.prev_ptr, self.stack_depth);
219285
}
220286
}

0 commit comments

Comments
 (0)
Please sign in to comment.