Skip to content

Commit 492a8fe

Browse files
committed
Address potential unsoundness during destruction
1 parent d9e0637 commit 492a8fe

File tree

4 files changed

+125
-5
lines changed

4 files changed

+125
-5
lines changed

compile-tests/tests/compile-fail/reborrow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ extern crate ref_mut_stack;
22

33
use ref_mut_stack::{ParkableRefMut, RefMutStack};
44

5-
fn main() -> () {
5+
fn main() {
66
let mut root = ();
77

88
struct Type<'a>(ParkableRefMut<'a, (), Self>);

justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ test *args:
2121
compile-test *args:
2222
cargo test --all-features {{args}}
2323

24-
miri:
25-
cargo miri test --all-features
24+
miri *args:
25+
cargo miri test --all-features {{args}}
2626

2727
check_all:
2828
just stable

src/lib.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ use std::{
55

66
pub struct RefMutStack<'a, R, T> {
77
root_ref: NonNull<R>,
8-
stack: Vec<(T, NonNull<R>)>,
8+
stack: SafeDropVec<(T, NonNull<R>)>,
99
_a: std::marker::PhantomData<&'a ()>,
1010
}
1111

1212
impl<'a, R, T> RefMutStack<'a, R, T> {
1313
pub fn new(r: &'a mut R) -> Self {
1414
Self {
1515
root_ref: NonNull::from_mut(r),
16-
stack: Vec::new(),
16+
stack: Vec::new().into(),
1717
_a: Default::default(),
1818
}
1919
}
@@ -27,6 +27,46 @@ impl<'a, R, T> RefMutStack<'a, R, T> {
2727
}
2828
}
2929

30+
/// This is a Vec which will drop its elements in reverse order on destruction.
31+
///
32+
/// It ensures soundness of error cases when the elements holding the mutable references really
33+
/// want to access them during their destruction. See soundness integration tests.
34+
///
35+
/// Note that is would be an anti-pattern to use the references in destructors, but this wrapper
36+
/// addresses incorrect implementations.
37+
///
38+
/// Also note that the borrow checker will not allow us to implement `Drop` on [`RefMutStack`]
39+
/// itself because of its self referencing nature. The borrow checker does not complain on the
40+
/// implementation but on the use site when it needs to drop the stack and realizes there are
41+
/// conflicting lifetime requirements.
42+
struct SafeDropVec<T>(Vec<T>);
43+
44+
impl<T> From<Vec<T>> for SafeDropVec<T> {
45+
fn from(value: Vec<T>) -> Self {
46+
Self(value)
47+
}
48+
}
49+
50+
impl<T> Drop for SafeDropVec<T> {
51+
fn drop(&mut self) {
52+
// Drop elements in reverse order.
53+
while self.0.pop().is_some() {}
54+
}
55+
}
56+
57+
impl<T> Deref for SafeDropVec<T> {
58+
type Target = Vec<T>;
59+
fn deref(&self) -> &Self::Target {
60+
&self.0
61+
}
62+
}
63+
64+
impl<T> DerefMut for SafeDropVec<T> {
65+
fn deref_mut(&mut self) -> &mut Self::Target {
66+
&mut self.0
67+
}
68+
}
69+
3070
pub struct ParkableRefMut<'a, R, T> {
3171
r: NonNull<R>,
3272
stack: NonNull<RefMutStack<'a, R, T>>,

tests/impl_drop.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use ref_mut_stack::{ParkableRefMut, RefMutStack};
2+
3+
// We want to provide a way to the user to use the references in destructors.
4+
//
5+
// But because of the existence of such a destructor, the reference needs to be detached from
6+
// `self` while unparking. This is why it is wrapped with an `Option`.
7+
8+
struct Builder<'a>(Option<ParkableRefMut<'a, usize, Self>>, usize);
9+
10+
impl<'a> Builder<'a> {
11+
fn build(mut self) -> Option<Self> {
12+
let r = self.0.take().unwrap();
13+
r.unpark()
14+
}
15+
}
16+
17+
impl<'a> Drop for Builder<'a> {
18+
fn drop(&mut self) {
19+
if let Some(r) = self.0.as_mut() {
20+
**r = self.1
21+
};
22+
}
23+
}
24+
25+
#[test]
26+
pub fn test_sound_incomplete_unstacking() {
27+
let mut root = 12;
28+
29+
{
30+
let mut stack = RefMutStack::<usize, Builder>::new(&mut root);
31+
let mut b1 = Builder(Some(stack.borrow_mut()), 100);
32+
let mut b2 = Builder(Some(b1.0.as_mut().unwrap().parker().park(b1, |r| r)), 200);
33+
let mut b3 = Builder(Some(b2.0.as_mut().unwrap().parker().park(b2, |r| r)), 300);
34+
let b4 = Builder(Some(b3.0.as_mut().unwrap().parker().park(b3, |r| r)), 400);
35+
36+
let Some(_b3) = b4.build() else {
37+
panic!("b4 build should return b3")
38+
};
39+
// `b3`, `b2` and `b1` are not built.
40+
//
41+
// The last 2 are still in the stack and need to be dropped in order for the process to be
42+
// sound.
43+
//
44+
// Everything is dropped here.
45+
}
46+
47+
// If not 100 that means the destructors are not called in the right order, which indicates
48+
// unsound behaviour (detected by Miri).
49+
assert_eq!(root, 100);
50+
}
51+
52+
#[test]
53+
pub fn test_sound_unwinding() {
54+
// Need a lock to cross the catch_unwind scope safely
55+
let root = std::sync::Mutex::new(12);
56+
57+
std::panic::catch_unwind(|| {
58+
let mut root = root.lock().unwrap();
59+
60+
let mut stack = RefMutStack::<usize, Builder>::new(&mut root);
61+
let mut b1 = Builder(Some(stack.borrow_mut()), 100);
62+
let mut b2 = Builder(Some(b1.0.as_mut().unwrap().parker().park(b1, |r| r)), 200);
63+
let mut b3 = Builder(Some(b2.0.as_mut().unwrap().parker().park(b2, |r| r)), 300);
64+
let b4 = Builder(Some(b3.0.as_mut().unwrap().parker().park(b3, |r| r)), 400);
65+
66+
// Drop root to make sure the lock is not poisoned
67+
drop(b4);
68+
drop(root);
69+
70+
// 3 builders are still in the stack and need to be dropped in order for the process to be
71+
// sound.
72+
73+
panic!("intentional panic");
74+
})
75+
.unwrap_err();
76+
77+
// If not 100 that means the destructors are not called in the right order, which indicates
78+
// unsound behaviour (detected by Miri).
79+
assert_eq!(*root.lock().unwrap(), 100);
80+
}

0 commit comments

Comments
 (0)