Skip to content

Commit 7fb8122

Browse files
authored
Rollup merge of rust-lang#123803 - Sp00ph:shrink_to_fix, r=Mark-Simulacrum
Fix `VecDeque::shrink_to` UB when `handle_alloc_error` unwinds. Fixes rust-lang#123369 For `VecDeque` it's relatively simple to restore the buffer into a consistent state so this PR does just that. Note that with its current implementation, `shrink_to` may change the internal arrangement of elements in the buffer, so e.g. `[D, <uninit>, A, B, C]` will become `[<uninit>, A, B, C, D]` and `[<uninit>, <uninit>, A, B, C]` may become `[B, C, <uninit>, <uninit>, A]` if `shrink_to` unwinds. This shouldn't be an issue though as we don't make any guarantees about the stability of the internal buffer arrangement (and this case is impossible to hit on stable anyways). This PR also includes a test with code adapted from rust-lang#123369 which fails without the new `shrink_to` code. Does this suffice or do we maybe need more exhaustive tests like in rust-lang#108475? cc `@Amanieu` `@rustbot` label +T-libs
2 parents 2a1b632 + 5cb53bc commit 7fb8122

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

library/alloc/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ rand_xorshift = "0.3.0"
2020
name = "alloctests"
2121
path = "tests/lib.rs"
2222

23+
[[test]]
24+
name = "vec_deque_alloc_error"
25+
path = "tests/vec_deque_alloc_error.rs"
26+
2327
[[bench]]
2428
name = "allocbenches"
2529
path = "benches/lib.rs"

library/alloc/src/collections/vec_deque/mod.rs

+65-1
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,8 @@ impl<T, A: Allocator> VecDeque<T, A> {
982982
// `head` and `len` are at most `isize::MAX` and `target_cap < self.capacity()`, so nothing can
983983
// overflow.
984984
let tail_outside = (target_cap + 1..=self.capacity()).contains(&(self.head + self.len));
985+
// Used in the drop guard below.
986+
let old_head = self.head;
985987

986988
if self.len == 0 {
987989
self.head = 0;
@@ -1034,12 +1036,74 @@ impl<T, A: Allocator> VecDeque<T, A> {
10341036
}
10351037
self.head = new_head;
10361038
}
1037-
self.buf.shrink_to_fit(target_cap);
1039+
1040+
struct Guard<'a, T, A: Allocator> {
1041+
deque: &'a mut VecDeque<T, A>,
1042+
old_head: usize,
1043+
target_cap: usize,
1044+
}
1045+
1046+
impl<T, A: Allocator> Drop for Guard<'_, T, A> {
1047+
#[cold]
1048+
fn drop(&mut self) {
1049+
unsafe {
1050+
// SAFETY: This is only called if `buf.shrink_to_fit` unwinds,
1051+
// which is the only time it's safe to call `abort_shrink`.
1052+
self.deque.abort_shrink(self.old_head, self.target_cap)
1053+
}
1054+
}
1055+
}
1056+
1057+
let guard = Guard { deque: self, old_head, target_cap };
1058+
1059+
guard.deque.buf.shrink_to_fit(target_cap);
1060+
1061+
// Don't drop the guard if we didn't unwind.
1062+
mem::forget(guard);
10381063

10391064
debug_assert!(self.head < self.capacity() || self.capacity() == 0);
10401065
debug_assert!(self.len <= self.capacity());
10411066
}
10421067

1068+
/// Reverts the deque back into a consistent state in case `shrink_to` failed.
1069+
/// This is necessary to prevent UB if the backing allocator returns an error
1070+
/// from `shrink` and `handle_alloc_error` subsequently unwinds (see #123369).
1071+
///
1072+
/// `old_head` refers to the head index before `shrink_to` was called. `target_cap`
1073+
/// is the capacity that it was trying to shrink to.
1074+
unsafe fn abort_shrink(&mut self, old_head: usize, target_cap: usize) {
1075+
// Moral equivalent of self.head + self.len <= target_cap. Won't overflow
1076+
// because `self.len <= target_cap`.
1077+
if self.head <= target_cap - self.len {
1078+
// The deque's buffer is contiguous, so no need to copy anything around.
1079+
return;
1080+
}
1081+
1082+
// `shrink_to` already copied the head to fit into the new capacity, so this won't overflow.
1083+
let head_len = target_cap - self.head;
1084+
// `self.head > target_cap - self.len` => `self.len > target_cap - self.head =: head_len` so this must be positive.
1085+
let tail_len = self.len - head_len;
1086+
1087+
if tail_len <= cmp::min(head_len, self.capacity() - target_cap) {
1088+
// There's enough spare capacity to copy the tail to the back (because `tail_len < self.capacity() - target_cap`),
1089+
// and copying the tail should be cheaper than copying the head (because `tail_len <= head_len`).
1090+
1091+
unsafe {
1092+
// The old tail and the new tail can't overlap because the head slice lies between them. The
1093+
// head slice ends at `target_cap`, so that's where we copy to.
1094+
self.copy_nonoverlapping(0, target_cap, tail_len);
1095+
}
1096+
} else {
1097+
// Either there's not enough spare capacity to make the deque contiguous, or the head is shorter than the tail
1098+
// (and therefore hopefully cheaper to copy).
1099+
unsafe {
1100+
// The old and the new head slice can overlap, so we can't use `copy_nonoverlapping` here.
1101+
self.copy(self.head, old_head, head_len);
1102+
self.head = old_head;
1103+
}
1104+
}
1105+
}
1106+
10431107
/// Shortens the deque, keeping the first `len` elements and dropping
10441108
/// the rest.
10451109
///
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#![feature(alloc_error_hook, allocator_api)]
2+
3+
use std::{
4+
alloc::{set_alloc_error_hook, AllocError, Allocator, Layout, System},
5+
collections::VecDeque,
6+
panic::{catch_unwind, AssertUnwindSafe},
7+
ptr::NonNull,
8+
};
9+
10+
#[test]
11+
fn test_shrink_to_unwind() {
12+
// This tests that `shrink_to` leaves the deque in a consistent state when
13+
// the call to `RawVec::shrink_to_fit` unwinds. The code is adapted from #123369
14+
// but changed to hopefully not have any UB even if the test fails.
15+
16+
struct BadAlloc;
17+
18+
unsafe impl Allocator for BadAlloc {
19+
fn allocate(&self, l: Layout) -> Result<NonNull<[u8]>, AllocError> {
20+
// We allocate zeroed here so that the whole buffer of the deque
21+
// is always initialized. That way, even if the deque is left in
22+
// an inconsistent state, no uninitialized memory should be accessed.
23+
System.allocate_zeroed(l)
24+
}
25+
26+
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
27+
unsafe { System.deallocate(ptr, layout) }
28+
}
29+
30+
unsafe fn shrink(
31+
&self,
32+
_ptr: NonNull<u8>,
33+
_old_layout: Layout,
34+
_new_layout: Layout,
35+
) -> Result<NonNull<[u8]>, AllocError> {
36+
Err(AllocError)
37+
}
38+
}
39+
40+
set_alloc_error_hook(|_| panic!("alloc error"));
41+
42+
let mut v = VecDeque::with_capacity_in(15, BadAlloc);
43+
v.push_back(1);
44+
v.push_front(2);
45+
// This should unwind because it calls `BadAlloc::shrink` and then `handle_alloc_error` which unwinds.
46+
assert!(catch_unwind(AssertUnwindSafe(|| v.shrink_to_fit())).is_err());
47+
// This should only pass if the deque is left in a consistent state.
48+
assert_eq!(v, [2, 1]);
49+
}

0 commit comments

Comments
 (0)