Skip to content

Commit d77b1cc

Browse files
committed
Fix VecDeque::shrink_to UB when handle_alloc_error unwinds.
Luckily it's comparatively simple to just restore the `VecDeque` into a valid state on unwinds.
1 parent 6eba9d7 commit d77b1cc

File tree

3 files changed

+120
-2
lines changed

3 files changed

+120
-2
lines changed

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
///

alloc/src/collections/vec_deque/tests.rs

+54-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
use core::iter::TrustedLen;
1+
#![feature(alloc_error_hook)]
2+
3+
use crate::alloc::{AllocError, Layout};
4+
use core::{iter::TrustedLen, ptr::NonNull};
5+
use std::{
6+
alloc::{set_alloc_error_hook, take_alloc_error_hook, System},
7+
panic::{catch_unwind, AssertUnwindSafe},
8+
};
29

310
use super::*;
411

@@ -790,6 +797,52 @@ fn test_shrink_to() {
790797
}
791798
}
792799

800+
#[test]
801+
fn test_shrink_to_unwind() {
802+
// This tests that `shrink_to` leaves the deque in a consistent state when
803+
// the call to `RawVec::shrink_to_fit` unwinds. The code is adapted from #123369
804+
// but changed to hopefully not have any UB even if the test fails.
805+
806+
struct BadAlloc;
807+
808+
unsafe impl Allocator for BadAlloc {
809+
fn allocate(&self, l: Layout) -> Result<NonNull<[u8]>, AllocError> {
810+
// We allocate zeroed here so that the whole buffer of the deque
811+
// is always initialized. That way, even if the deque is left in
812+
// an inconsistent state, no uninitialized memory should be accessed.
813+
System.allocate_zeroed(l)
814+
}
815+
816+
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
817+
unsafe { System.deallocate(ptr, layout) }
818+
}
819+
820+
unsafe fn shrink(
821+
&self,
822+
_ptr: NonNull<u8>,
823+
_old_layout: Layout,
824+
_new_layout: Layout,
825+
) -> Result<NonNull<[u8]>, AllocError> {
826+
Err(AllocError)
827+
}
828+
}
829+
830+
// preserve the old error hook just in case.
831+
let old_error_hook = take_alloc_error_hook();
832+
set_alloc_error_hook(|_| panic!("alloc error"));
833+
834+
let mut v = VecDeque::with_capacity_in(15, BadAlloc);
835+
v.push_back(1);
836+
v.push_front(2);
837+
// This should unwind because it calls `BadAlloc::shrink` and then `handle_alloc_error` which unwinds.
838+
assert!(catch_unwind(AssertUnwindSafe(|| v.shrink_to_fit())).is_err());
839+
// This should only pass if the deque is left in a consistent state.
840+
assert_eq!(v, [2, 1]);
841+
842+
// restore the old error hook.
843+
set_alloc_error_hook(old_error_hook);
844+
}
845+
793846
#[test]
794847
fn test_shrink_to_fit() {
795848
// This test checks that every single combination of head and tail position,

alloc/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
// tidy-alphabetical-start
9393
#![cfg_attr(not(no_global_oom_handling), feature(const_alloc_error))]
9494
#![cfg_attr(not(no_global_oom_handling), feature(const_btree_len))]
95+
#![cfg_attr(test, feature(alloc_error_hook))]
9596
#![cfg_attr(test, feature(is_sorted))]
9697
#![cfg_attr(test, feature(new_uninit))]
9798
#![feature(alloc_layout_extra)]

0 commit comments

Comments
 (0)