Skip to content

Commit fc109bb

Browse files
committed
Avoid zero-sized allocs in ThinBox if T and H are both ZSTs.
1 parent 764b861 commit fc109bb

File tree

3 files changed

+278
-19
lines changed

3 files changed

+278
-19
lines changed

library/alloc/src/boxed/thin.rs

+46-19
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,11 @@ impl<T: ?Sized> ThinBox<T> {
138138
}
139139
}
140140

141-
/// A pointer to type-erased data, guaranteed to have a header `H` before the pointed-to location.
141+
/// A pointer to type-erased data, guaranteed to either be:
142+
/// 1. `NonNull::dangling()`, in the case where both the pointee (`T`) and
143+
/// metadata (`H`) are ZSTs.
144+
/// 2. A pointer to a valid `T` that has a header `H` directly before the
145+
/// pointed-to location.
142146
struct WithHeader<H>(NonNull<u8>, PhantomData<H>);
143147

144148
impl<H> WithHeader<H> {
@@ -156,16 +160,27 @@ impl<H> WithHeader<H> {
156160
};
157161

158162
unsafe {
159-
let ptr = alloc::alloc(layout);
160-
161-
if ptr.is_null() {
162-
alloc::handle_alloc_error(layout);
163-
}
164-
// Safety:
165-
// - The size is at least `aligned_header_size`.
166-
let ptr = ptr.add(value_offset) as *mut _;
167-
168-
let ptr = NonNull::new_unchecked(ptr);
163+
// Note: It's UB to pass a layout with a zero size to `alloc::alloc`, so
164+
// we use `layout.dangling()` for this case, which should have a valid
165+
// alignment for both `T` and `H`.
166+
let ptr = if layout.size() == 0 {
167+
// Some paranoia checking, mostly so that the ThinBox tests are
168+
// more able to catch issues.
169+
debug_assert!(
170+
value_offset == 0 && mem::size_of::<T>() == 0 && mem::size_of::<H>() == 0
171+
);
172+
layout.dangling()
173+
} else {
174+
let ptr = alloc::alloc(layout);
175+
if ptr.is_null() {
176+
alloc::handle_alloc_error(layout);
177+
}
178+
// Safety:
179+
// - The size is at least `aligned_header_size`.
180+
let ptr = ptr.add(value_offset) as *mut _;
181+
182+
NonNull::new_unchecked(ptr)
183+
};
169184

170185
let result = WithHeader(ptr, PhantomData);
171186
ptr::write(result.header(), header);
@@ -175,18 +190,28 @@ impl<H> WithHeader<H> {
175190
}
176191
}
177192

178-
// Safety:
179-
// - Assumes that `value` can be dereferenced.
193+
// Safety:
194+
// - Assumes that either `value` can be dereferenced, or is the
195+
// `NonNull::dangling()` we use when both `T` and `H` are ZSTs.
180196
unsafe fn drop<T: ?Sized>(&self, value: *mut T) {
181197
unsafe {
198+
let value_layout = Layout::for_value_raw(value);
182199
// SAFETY: Layout must have been computable if we're in drop
183-
let (layout, value_offset) =
184-
Self::alloc_layout(Layout::for_value_raw(value)).unwrap_unchecked();
200+
let (layout, value_offset) = Self::alloc_layout(value_layout).unwrap_unchecked();
185201

186-
ptr::drop_in_place::<T>(value);
187202
// We only drop the value because the Pointee trait requires that the metadata is copy
188-
// aka trivially droppable
189-
alloc::dealloc(self.0.as_ptr().sub(value_offset), layout);
203+
// aka trivially droppable.
204+
ptr::drop_in_place::<T>(value);
205+
206+
// Note: Don't deallocate if the layout size is zero, because the pointer
207+
// didn't come from the allocator.
208+
if layout.size() != 0 {
209+
alloc::dealloc(self.0.as_ptr().sub(value_offset), layout);
210+
} else {
211+
debug_assert!(
212+
value_offset == 0 && mem::size_of::<H>() == 0 && value_layout.size() == 0
213+
);
214+
}
190215
}
191216
}
192217

@@ -198,7 +223,9 @@ impl<H> WithHeader<H> {
198223
// needed to align the header. Subtracting the header size from the aligned data pointer
199224
// will always result in an aligned header pointer, it just may not point to the
200225
// beginning of the allocation.
201-
unsafe { self.0.as_ptr().sub(Self::header_size()) as *mut H }
226+
let hp = unsafe { self.0.as_ptr().sub(Self::header_size()) as *mut H };
227+
debug_assert!((hp.addr() & (core::mem::align_of::<H>() - 1)) == 0);
228+
hp
202229
}
203230

204231
fn value(&self) -> *mut u8 {

library/alloc/tests/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
#![feature(panic_update_hook)]
4343
#![feature(slice_flatten)]
4444
#![feature(thin_box)]
45+
#![feature(bench_black_box)]
46+
#![feature(strict_provenance)]
47+
#![feature(once_cell)]
4548

4649
use std::collections::hash_map::DefaultHasher;
4750
use std::hash::{Hash, Hasher};

library/alloc/tests/thin_box.rs

+229
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use core::fmt::Debug;
12
use core::mem::size_of;
23
use std::boxed::ThinBox;
34

@@ -24,3 +25,231 @@ fn want_thin() {
2425
assert!(is_thin::<[i32]>());
2526
assert!(is_thin::<i32>());
2627
}
28+
29+
#[track_caller]
30+
fn verify_aligned<T>(ptr: *const T) {
31+
// Use `black_box` to attempt to obscure the fact that we're calling this
32+
// function on pointers that come from box/references, which the compiler
33+
// would otherwise realize is impossible (because it would mean we've
34+
// already executed UB).
35+
//
36+
// That is, we'd *like* it to be possible for the asserts in this function
37+
// to detect brokenness in the ThinBox impl.
38+
//
39+
// It would probably be better if we instead had these as debug_asserts
40+
// inside `ThinBox`, prior to the point where we do the UB. Anyway, in
41+
// practice these checks are mostly just smoke-detectors for an extremely
42+
// broken `ThinBox` impl, since it's an extremely subtle piece of code.
43+
let ptr = core::hint::black_box(ptr);
44+
let align = core::mem::align_of::<T>();
45+
assert!(
46+
(ptr.addr() & (align - 1)) == 0 && !ptr.is_null(),
47+
"misaligned ThinBox data; valid pointers to `{}` should be aligned to {align}: {ptr:p}",
48+
core::any::type_name::<T>(),
49+
);
50+
}
51+
52+
#[track_caller]
53+
fn check_thin_sized<T: Debug + PartialEq + Clone>(make: impl FnOnce() -> T) {
54+
let value = make();
55+
let boxed = ThinBox::new(value.clone());
56+
let val = &*boxed;
57+
verify_aligned(val as *const T);
58+
assert_eq!(val, &value);
59+
}
60+
61+
#[track_caller]
62+
fn check_thin_dyn<T: Debug + PartialEq + Clone>(make: impl FnOnce() -> T) {
63+
let value = make();
64+
let wanted_debug = format!("{value:?}");
65+
let boxed: ThinBox<dyn Debug> = ThinBox::new_unsize(value.clone());
66+
let val = &*boxed;
67+
// wide reference -> wide pointer -> thin pointer
68+
verify_aligned(val as *const dyn Debug as *const T);
69+
let got_debug = format!("{val:?}");
70+
assert_eq!(wanted_debug, got_debug);
71+
}
72+
73+
macro_rules! define_test {
74+
(
75+
@test_name: $testname:ident;
76+
77+
$(#[$m:meta])*
78+
struct $Type:ident($inner:ty);
79+
80+
$($test_stmts:tt)*
81+
) => {
82+
#[test]
83+
fn $testname() {
84+
use core::sync::atomic::{AtomicIsize, Ordering};
85+
// Define the type, and implement new/clone/drop in such a way that
86+
// the number of live instances will be counted.
87+
$(#[$m])*
88+
#[derive(Debug, PartialEq)]
89+
struct $Type {
90+
_priv: $inner,
91+
}
92+
93+
impl Clone for $Type {
94+
fn clone(&self) -> Self {
95+
verify_aligned(self);
96+
Self::new(self._priv.clone())
97+
}
98+
}
99+
100+
impl Drop for $Type {
101+
fn drop(&mut self) {
102+
verify_aligned(self);
103+
Self::modify_live(-1);
104+
}
105+
}
106+
107+
impl $Type {
108+
fn new(i: $inner) -> Self {
109+
Self::modify_live(1);
110+
Self { _priv: i }
111+
}
112+
113+
fn modify_live(n: isize) -> isize {
114+
static COUNTER: AtomicIsize = AtomicIsize::new(0);
115+
COUNTER.fetch_add(n, Ordering::Relaxed) + n
116+
}
117+
118+
fn live_objects() -> isize {
119+
Self::modify_live(0)
120+
}
121+
}
122+
// Run the test statements
123+
let _: () = { $($test_stmts)* };
124+
// Check that we didn't leak anything, or call drop too many times.
125+
assert_eq!(
126+
$Type::live_objects(), 0,
127+
"Wrong number of drops of {}, `initializations - drops` should be 0.",
128+
stringify!($Type),
129+
);
130+
}
131+
};
132+
}
133+
134+
define_test! {
135+
@test_name: align1zst;
136+
struct Align1Zst(());
137+
138+
check_thin_sized(|| Align1Zst::new(()));
139+
check_thin_dyn(|| Align1Zst::new(()));
140+
}
141+
142+
define_test! {
143+
@test_name: align1small;
144+
struct Align1Small(u8);
145+
146+
check_thin_sized(|| Align1Small::new(50));
147+
check_thin_dyn(|| Align1Small::new(50));
148+
}
149+
150+
define_test! {
151+
@test_name: align1_size_not_pow2;
152+
struct Align64NotPow2Size([u8; 79]);
153+
154+
check_thin_sized(|| Align64NotPow2Size::new([100; 79]));
155+
check_thin_dyn(|| Align64NotPow2Size::new([100; 79]));
156+
}
157+
158+
define_test! {
159+
@test_name: align1big;
160+
struct Align1Big([u8; 256]);
161+
162+
check_thin_sized(|| Align1Big::new([5u8; 256]));
163+
check_thin_dyn(|| Align1Big::new([5u8; 256]));
164+
}
165+
166+
// Note: `#[repr(align(2))]` is worth testing because
167+
// - can have pointers which are misaligned, unlike align(1)
168+
// - is still expected to have an alignment less than the alignment of a vtable.
169+
define_test! {
170+
@test_name: align2zst;
171+
#[repr(align(2))]
172+
struct Align2Zst(());
173+
174+
check_thin_sized(|| Align2Zst::new(()));
175+
check_thin_dyn(|| Align2Zst::new(()));
176+
}
177+
178+
define_test! {
179+
@test_name: align2small;
180+
#[repr(align(2))]
181+
struct Align2Small(u8);
182+
183+
check_thin_sized(|| Align2Small::new(60));
184+
check_thin_dyn(|| Align2Small::new(60));
185+
}
186+
187+
define_test! {
188+
@test_name: align2full;
189+
#[repr(align(2))]
190+
struct Align2Full([u8; 2]);
191+
check_thin_sized(|| Align2Full::new([3u8; 2]));
192+
check_thin_dyn(|| Align2Full::new([3u8; 2]));
193+
}
194+
195+
define_test! {
196+
@test_name: align2_size_not_pow2;
197+
#[repr(align(2))]
198+
struct Align2NotPower2Size([u8; 6]);
199+
200+
check_thin_sized(|| Align2NotPower2Size::new([3; 6]));
201+
check_thin_dyn(|| Align2NotPower2Size::new([3; 6]));
202+
}
203+
204+
define_test! {
205+
@test_name: align2big;
206+
#[repr(align(2))]
207+
struct Align2Big([u8; 256]);
208+
209+
check_thin_sized(|| Align2Big::new([5u8; 256]));
210+
check_thin_dyn(|| Align2Big::new([5u8; 256]));
211+
}
212+
213+
define_test! {
214+
@test_name: align64zst;
215+
#[repr(align(64))]
216+
struct Align64Zst(());
217+
218+
check_thin_sized(|| Align64Zst::new(()));
219+
check_thin_dyn(|| Align64Zst::new(()));
220+
}
221+
222+
define_test! {
223+
@test_name: align64small;
224+
#[repr(align(64))]
225+
struct Align64Small(u8);
226+
227+
check_thin_sized(|| Align64Small::new(50));
228+
check_thin_dyn(|| Align64Small::new(50));
229+
}
230+
231+
define_test! {
232+
@test_name: align64med;
233+
#[repr(align(64))]
234+
struct Align64Med([u8; 64]);
235+
check_thin_sized(|| Align64Med::new([10; 64]));
236+
check_thin_dyn(|| Align64Med::new([10; 64]));
237+
}
238+
239+
define_test! {
240+
@test_name: align64_size_not_pow2;
241+
#[repr(align(64))]
242+
struct Align64NotPow2Size([u8; 192]);
243+
244+
check_thin_sized(|| Align64NotPow2Size::new([10; 192]));
245+
check_thin_dyn(|| Align64NotPow2Size::new([10; 192]));
246+
}
247+
248+
define_test! {
249+
@test_name: align64big;
250+
#[repr(align(64))]
251+
struct Align64Big([u8; 256]);
252+
253+
check_thin_sized(|| Align64Big::new([10; 256]));
254+
check_thin_dyn(|| Align64Big::new([10; 256]));
255+
}

0 commit comments

Comments
 (0)