Skip to content

Commit aa98c2a

Browse files
committed
allocator::c::{,Aligned}Malloc: fix ZST (re)allocs by replacing them with alloc(...)?; free(...);
relates to #32
1 parent 730603d commit aa98c2a

File tree

3 files changed

+88
-26
lines changed

3 files changed

+88
-26
lines changed

src/allocator/c/aligned_malloc.rs

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,23 @@ impl Meta for AlignedMalloc {
7878

7979
const MAX_SIZE : usize = usize::MAX;
8080

81-
/// | Platform | Behavior |
82-
/// | ------------------| ---------|
83-
/// | Linux | Succeeds?
84-
/// | Windows | Fails? [`_aligned_malloc`] explicitly documents "If \[...\] `size` is zero, this function invokes the invalid parameter handler, as described in [Parameter validation](https://learn.microsoft.com/en-us/cpp/c-runtime-library/parameter-validation). If execution is allowed to continue, this function returns `NULL` and sets `errno` to `EINVAL`."
81+
/// ### Platform: OS X
82+
/// Zero sized allocs succeed.
83+
/// Reallocs are untested.
84+
///
85+
/// ### Platform: Linux
86+
/// Zero sized allocs succeed.
87+
/// Reallocs are untested.
88+
///
89+
/// ### Platform: Windows
90+
///
91+
/// [`_aligned_malloc`] is documented as failing when size = 0.
92+
/// This is a lie: it succeeds, at least on 10.0.19045.5737.
93+
/// OTOH, it may or may not trigger debug checks on other versions of windows or the CRT.
94+
///
95+
/// [`_aligned_realloc`], on the other hand, is documented as *freeing* when size = 0.
96+
/// Tests seem to confirm it, resulting in heap corruption checks if the buffers are sufficiently used afterwards.
97+
/// As such, `*::Realloc` will skip it in favor of [`_aligned_malloc`] and [`_aligned_free`] when size = 0.
8598
///
8699
#[doc = include_str!("_refs.md")]
87100
const ZST_SUPPORTED : bool = false;
@@ -174,21 +187,33 @@ unsafe impl fat::Free for AlignedMalloc {
174187
// SAFETY: per above
175188
unsafe impl fat::Realloc for AlignedMalloc {
176189
#[cfg(target_env = "msvc")]
177-
#[track_caller] unsafe fn realloc_uninit(&self, ptr: AllocNN, _old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
190+
#[track_caller] unsafe fn realloc_uninit(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
178191
let new_layout = Self::fix_layout(new_layout)?;
179-
// SAFETY: ✔️ `ptr` belongs to `self` per [`fat::Realloc::realloc_uninit`]'s documented safety preconditions
180-
// SAFETY: ✔️ `new_layout` has been checked/fixed for platform validity
181-
let alloc = unsafe { ffi::_aligned_realloc(ptr.as_ptr().cast(), new_layout.size(), new_layout.align()) };
182-
NonNull::new(alloc.cast()).ok_or(())
192+
if new_layout.size() == 0 {
193+
let alloc = fat::Alloc::alloc_uninit(self, new_layout)?;
194+
unsafe { fat::Free::free(self, ptr, old_layout) };
195+
Ok(alloc)
196+
} else {
197+
// SAFETY: ✔️ `ptr` belongs to `self` per [`fat::Realloc::realloc_uninit`]'s documented safety preconditions
198+
// SAFETY: ✔️ `new_layout` has been checked/fixed for platform validity
199+
let alloc = unsafe { ffi::_aligned_realloc(ptr.as_ptr().cast(), new_layout.size(), new_layout.align()) };
200+
NonNull::new(alloc.cast()).ok_or(())
201+
}
183202
}
184203

185204
#[cfg(target_env = "msvc")]
186-
unsafe fn realloc_zeroed(&self, ptr: AllocNN, _old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
205+
unsafe fn realloc_zeroed(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
187206
let new_layout = Self::fix_layout(new_layout)?;
188-
// SAFETY: ✔️ `ptr` belongs to `self` per [`fat::Realloc::realloc_zeroed`]'s documented safety preconditions
189-
// SAFETY: ✔️ `new_layout` has been checked/fixed for platform validity
190-
let alloc = unsafe { ffi::_aligned_recalloc(ptr.as_ptr().cast(), 1, new_layout.size(), new_layout.align()) };
191-
NonNull::new(alloc.cast()).ok_or(())
207+
if new_layout.size() == 0 {
208+
let alloc = fat::Alloc::alloc_zeroed(self, new_layout)?;
209+
unsafe { fat::Free::free(self, ptr, old_layout) };
210+
Ok(alloc.cast())
211+
} else {
212+
// SAFETY: ✔️ `ptr` belongs to `self` per [`fat::Realloc::realloc_zeroed`]'s documented safety preconditions
213+
// SAFETY: ✔️ `new_layout` has been checked/fixed for platform validity
214+
let alloc = unsafe { ffi::_aligned_recalloc(ptr.as_ptr().cast(), 1, new_layout.size(), new_layout.align()) };
215+
NonNull::new(alloc.cast()).ok_or(())
216+
}
192217
}
193218
}
194219

@@ -267,5 +292,7 @@ mod ffi {
267292
#[test] fn fat_alignment() { fat::test::alignment(AlignedMalloc) }
268293
#[test] fn fat_edge_case_sizes() { fat::test::edge_case_sizes(AlignedMalloc) }
269294
#[test] fn fat_uninit() { if !ALIGNED_ALLOC_ZERO_INITS { unsafe { fat::test::uninit_alloc_unsound(AlignedMalloc) } } }
295+
#[test] fn fat_uninit_realloc() { fat::test::uninit_realloc(AlignedMalloc) }
270296
#[test] fn fat_zeroed() { fat::test::zeroed_alloc(AlignedMalloc) }
297+
#[test] fn fat_zeroed_realloc() { fat::test::zeroed_realloc(AlignedMalloc) }
271298
#[test] fn fat_zst_support() { fat::test::zst_supported_conservative(AlignedMalloc) }

src/allocator/c/malloc.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,24 @@ impl Meta for Malloc {
4848

4949
const MAX_SIZE : usize = usize::MAX; // *slightly* less in practice
5050

51-
/// "If the size of the space requested is zero, the behavior is implementation defined: either a null pointer is returned, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object."
52-
/// C89 § 7.20.3 ¶ 1
51+
/// `malloc(0)` is implementation defined at best (C89 § 7.20.3 ¶ 1), but `realloc(ptr, 0)` is [straight up undefined behavior as of C23](https://en.cppreference.com/w/c/memory/realloc#:~:text=the%20behavior%20is%20undefined)?
52+
/// This wrapper works around that problem by invoking `malloc(0)` then `free(ptr)`.
5353
///
54-
/// Null pointers will be translated into an [`Err`], so this is at least defined behavior, but consider using an [`adapt`](crate::allocator::adapt) allocator for ZST support.
54+
/// Consider using an [`adapt`](crate::allocator::adapt) allocator for ZST support.
55+
///
56+
/// ### Platform: OS X
57+
/// - `malloc(0)` *untested?*
58+
/// - `realloc(ptr, 0)` *untested?*
59+
///
60+
/// ### Platform: Linux
61+
/// - `malloc(0)` allocates
62+
/// - `realloc(ptr, 0)` *untested?*
63+
///
64+
/// ### Platform: Windows
65+
/// - `malloc(0)` allocates
66+
/// - `_msize(zst)` returns `0`
67+
/// - `realloc(ptr, 0)` **frees** - to avoid this causing problems, this wrapper replaces it with `malloc(0)`, `free(ptr)`.
5568
///
56-
/// | Platform | Behavior |
57-
/// | ------------------| ----------|
58-
/// | Linux | Allocate
59-
/// | OS X | ???
60-
/// | Windows | Allocate
6169
const ZST_SUPPORTED : bool = false;
6270
}
6371

@@ -130,12 +138,24 @@ unsafe impl thin::Realloc for Malloc {
130138
const CAN_REALLOC_ZEROED : bool = cfg!(target_env = "msvc");
131139

132140
#[track_caller] unsafe fn realloc_uninit(&self, ptr: AllocNN, new_size: usize) -> Result<AllocNN, Self::Error> {
133-
// SAFETY: ✔️ `ptr` belongs to `self` per thin::Realloc's documented safety preconditions, and thus was allocated with one of `malloc`, `calloc`, `realloc, or `_recalloc` - all of which should be safe to `realloc`.
134-
let alloc = unsafe { realloc(ptr.as_ptr().cast(), new_size) };
135-
NonNull::new(alloc.cast()).ok_or(())
141+
if new_size == 0 { // see <Malloc as Meta>::ZST_SUPPORTED rant above
142+
let alloc = thin::Alloc::alloc_uninit(self, new_size)?;
143+
unsafe { thin::Free::free(self, ptr) };
144+
Ok(alloc)
145+
} else {
146+
// SAFETY: ✔️ `ptr` belongs to `self` per thin::Realloc's documented safety preconditions, and thus was allocated with one of `malloc`, `calloc`, `realloc, or `_recalloc` - all of which should be safe to `realloc`.
147+
let alloc = unsafe { realloc(ptr.as_ptr().cast(), new_size) };
148+
NonNull::new(alloc.cast()).ok_or(())
149+
}
136150
}
137151

138152
#[track_caller] unsafe fn realloc_zeroed(&self, ptr: AllocNN, new_size: usize) -> Result<AllocNN, Self::Error> {
153+
if Self::CAN_REALLOC_ZEROED && new_size == 0 { // see <Malloc as Meta>::ZST_SUPPORTED rant above
154+
let alloc = thin::Alloc::alloc_zeroed(self, new_size)?;
155+
unsafe { thin::Free::free(self, ptr) };
156+
return Ok(alloc.cast());
157+
}
158+
139159
#[cfg(target_env = "msvc")] {
140160
// https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/recalloc
141161
extern "C" { fn _recalloc(memblock: *mut c_void, num: size_t, size: size_t) -> *mut c_void; }
@@ -195,17 +215,22 @@ unsafe impl thin::SizeOfDebug for Malloc {
195215
#[test] fn thin_alignment() { thin::test::alignment(Malloc) }
196216
#[test] fn thin_edge_case_sizes() { thin::test::edge_case_sizes(Malloc) }
197217
#[test] fn thin_nullable() { thin::test::nullable(Malloc) }
198-
#[test] fn thin_size() { thin::test::size_over_alloc(Malloc) }
218+
#[cfg( target_env = "msvc" )] #[test] fn thin_size_msvc() { thin::test::size_exact_alloc_except_zsts(Malloc) }
219+
#[cfg(not(target_env = "msvc"))] #[test] fn thin_size() { thin::test::size_exact_alloc(Malloc) }
199220
#[test] fn thin_uninit() { if !MALLOC_ZERO_INITS { unsafe { thin::test::uninit_alloc_unsound(Malloc) } } }
221+
#[test] fn thin_uninit_realloc() { thin::test::uninit_realloc(Malloc) }
200222
#[test] fn thin_zeroed() { thin::test::zeroed_alloc(Malloc) }
223+
#[test] fn thin_zeroed_realloc() { thin::test::zeroed_realloc(Malloc) }
201224
#[test] fn thin_zst_support() { thin::test::zst_supported_conservative(Malloc) }
202225
#[test] fn thin_zst_support_dangle() { thin::test::zst_supported_conservative(crate::allocator::adapt::DangleZst(Malloc)) }
203226
#[test] fn thin_zst_support_alloc() { thin::test::zst_supported_conservative(crate::allocator::adapt::AllocZst(Malloc)) }
204227

205228
#[test] fn fat_alignment() { fat::test::alignment(Malloc) }
206229
#[test] fn fat_edge_case_sizes() { fat::test::edge_case_sizes(Malloc) }
207230
#[test] fn fat_uninit() { if !MALLOC_ZERO_INITS { unsafe { fat::test::uninit_alloc_unsound(Malloc) } } }
231+
#[test] fn fat_uninit_realloc() { fat::test::uninit_realloc(Malloc) }
208232
#[test] fn fat_zeroed() { fat::test::zeroed_alloc(Malloc) }
233+
#[test] fn fat_zeroed_realloc() { fat::test::zeroed_realloc(Malloc) }
209234
#[test] fn fat_zst_support() { fat::test::zst_supported_conservative(Malloc) }
210235
#[test] fn fat_zst_support_dangle() { fat::test::zst_supported_conservative(crate::allocator::adapt::DangleZst(Malloc)) }
211236
#[test] fn fat_zst_support_alloc() { fat::test::zst_supported_conservative(crate::allocator::adapt::AllocZst(Malloc)) }

src/traits/thin.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ pub mod test {
352352
}
353353
}
354354

355+
/// Assert that `allocator` always reports an exact allocation size, except for ZSTs
356+
pub fn size_exact_alloc_except_zsts<A: Alloc + Free + SizeOfDebug>(allocator: A) {
357+
for size in [0, 1, 3, 7, 15, 31, 63, 127] {
358+
let Ok(alloc) = TTB::try_new_uninit(&allocator, size) else { continue };
359+
// SAFETY: ✔️ `alloc` belongs to `allocator`
360+
let query_size = unsafe { allocator.size_of_debug(alloc.as_nonnull()) }.unwrap_or(size);
361+
assert_eq!(size.max(1), query_size, "allocator returns oversized allocs, use thin::test::size_over_alloc instead");
362+
}
363+
}
364+
355365
/// Assert that `allocator` sometimes reports a larger-than-requested allocation size
356366
pub fn size_over_alloc<A: Alloc + Free + SizeOfDebug>(allocator: A) {
357367
let mut any_sized = false;

0 commit comments

Comments
 (0)