diff --git a/Cargo.toml b/Cargo.toml index a5a52a3d4..68ae4ac11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -303,12 +303,6 @@ rust.unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(miri_race_coverage)', 'cfg(miri_strict_provenance)', 'cfg(miri_tree_borrows)', - # Set when the codegen backend cannot give a type the `#[repr(align(N))]` it asks for, - # which makes `multitude`'s over-aligned test types fail to compile. Nothing in-tree sets - # it; a build using such a backend passes `--cfg align_capped_backend` itself. It is - # all-or-nothing: it drops every over-aligned case (32 KiB, 64 KiB and 128 KiB), so a - # backend that supports some of them still loses that coverage. - 'cfg(align_capped_backend)', ] } # <<< anvil-managed: anvil-workspace-lints diff --git a/crates/multitude/Cargo.toml b/crates/multitude/Cargo.toml index 7b396e293..a8b01e824 100644 --- a/crates/multitude/Cargo.toml +++ b/crates/multitude/Cargo.toml @@ -97,7 +97,7 @@ mutants = { workspace = true } serde = { workspace = true, features = ["alloc", "derive"] } serde_json = { workspace = true, features = ["alloc", "raw_value"] } widestring = { workspace = true, features = ["alloc"] } -zerocopy = { workspace = true } +zerocopy = { workspace = true, features = ["derive"] } [target.'cfg(loom)'.dev-dependencies] loom = { workspace = true } diff --git a/crates/multitude/src/allocator_impl.rs b/crates/multitude/src/allocator_impl.rs index be1ce130c..12a8056b8 100644 --- a/crates/multitude/src/allocator_impl.rs +++ b/crates/multitude/src/allocator_impl.rs @@ -9,13 +9,6 @@ use allocator_api2::alloc::{AllocError, Allocator}; use crate::Arena; use crate::arena::alloc_value::acquire_chunk_ref; use crate::internal::chunk_ref::ChunkRef; -use crate::internal::constants::max_smart_ptr_align; - -/// Exclusive upper bound on `layout.align()` accepted by -/// `Allocator::allocate`. Alignments at or above this value are rejected: the -/// returned pointer must lie strictly inside the first `CHUNK_ALIGN` bytes of -/// its chunk so the header-recovery mask can recover the chunk pointer. -const MAX_SMART_PTR_ALIGN: usize = max_smart_ptr_align(); /// `&Arena` is the allocator handle: cheap to copy and backed by /// chunks. `allocate` bumps the chunk refcount; `deallocate` @@ -34,7 +27,7 @@ unsafe impl Allocator for &Arena { // Reject alignments at/above the smart-pointer ceiling (as `alloc_box` / // `alloc_arc` do): the header-from-mask helper requires the value to lie // strictly inside the first `CHUNK_ALIGN` bytes of the chunk. - if layout.align() >= MAX_SMART_PTR_ALIGN { + if self.rejects_smart_ptr_align(layout.align()) { return Err(AllocError); } // Zero-byte allocations need a non-null, well-aligned pointer but no @@ -270,7 +263,7 @@ mod tests { unsafe { LegacyAllocator::deallocate(&handle, p.cast::(), empty_layout) }; // Unsupported alignment remains a recoverable allocator error. - let over_aligned = Layout::from_size_align(8, super::MAX_SMART_PTR_ALIGN).unwrap(); + let over_aligned = Layout::from_size_align(8, arena.smart_ptr_align_cap()).unwrap(); LegacyAllocator::allocate(&handle, over_aligned).expect_err("over-aligned request must be rejected"); } } diff --git a/crates/multitude/src/arena/align_guard_tests.rs b/crates/multitude/src/arena/align_guard_tests.rs new file mode 100644 index 000000000..456496190 --- /dev/null +++ b/crates/multitude/src/arena/align_guard_tests.rs @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Every allocation entry point rejects a type aligned at or above the +//! arena's cap. +//! +//! Smart pointers recover their chunk header by masking the value +//! pointer's offset within its chunk tile. A value aligned at or above +//! the cap can land outside the first tile, where the mask recovers a +//! different chunk's header and `Drop` corrupts it. The guards make that +//! unreachable from safe code, and these tests hold every entry point to +//! it. +//! +//! The tests run against [`capped_arena`], whose caps are lowered so the +//! boundary is reachable by an alignment every codegen backend accepts. + +use crate::Arena; +use crate::internal::constants::{CHUNK_ALIGN, max_smart_ptr_align}; +use crate::tests_support::{ChunkOverAligned, SmartPtrOverAligned, SmartPtrOverAlignedDrop, TEST_CHUNK_ALIGN, capped_arena}; + +// The guards read their cap from the arena; `buffer_freezable` still reads +// `max_smart_ptr_align()` directly. The two must agree, or a `Vec` would +// freeze in place for an element the smart-pointer path rejects. +#[test] +fn default_caps_match_the_constants() { + let arena = Arena::new(); + assert_eq!(arena.chunk_align_cap(), CHUNK_ALIGN); + assert_eq!(arena.smart_ptr_align_cap(), max_smart_ptr_align()); +} + +#[test] +#[should_panic(expected = "the cap may only be lowered")] +fn set_align_cap_rejects_raising_the_cap() { + // Must stay a power of two, or the earlier assert fires instead. + Arena::new().set_align_cap(CHUNK_ALIGN * 2); +} + +#[test] +#[should_panic(expected = "alignment cap must leave room")] +fn set_align_cap_rejects_a_degenerate_cap() { + Arena::new().set_align_cap(1); +} + +// The guards compare `>=`, so they must reject alignments strictly above the +// cap as well as those exactly at it. Every fixture is aligned exactly at a +// cap, so reaching "strictly above" means lowering the cap further rather +// than raising an alignment. +#[test] +fn guards_reject_alignments_above_the_cap() { + let arena = capped_arena(); + arena.set_align_cap(TEST_CHUNK_ALIGN / 2); + + assert!(arena.rejects_smart_ptr_align(align_of::())); + assert!(arena.rejects_chunk_align(align_of::())); + + arena.try_alloc_with(|| SmartPtrOverAligned(0)).unwrap_err(); + + let src = [ChunkOverAligned(0)]; + arena.try_alloc_slice_copy(&src[..]).unwrap_err(); +} + +// The counterpart to the above: an alignment strictly below the cap is +// accepted, so the guards are not simply rejecting everything. +#[test] +fn guards_accept_alignments_below_the_cap() { + let arena = capped_arena(); + + assert!(!arena.rejects_smart_ptr_align(align_of::())); + assert!(!arena.rejects_chunk_align(align_of::())); +} + +#[test] +fn rejection_reports_alignment_too_large() { + let arena = capped_arena(); + let err = arena.try_alloc_with(|| SmartPtrOverAligned(0)).unwrap_err(); + assert!(err.is_alignment_too_large()); + + let err = arena.try_alloc_arc_with(|| SmartPtrOverAlignedDrop(0)).unwrap_err(); + assert!(err.is_alignment_too_large()); + + // A local binding, not `&[…][..]`: an over-aligned temporary that gets + // const-promoted lands in a section the linker rejects. + let src = [ChunkOverAligned(0)]; + let err = arena.try_alloc_slice_copy(&src[..]).unwrap_err(); + assert!(err.is_alignment_too_large()); +} + +#[test] +fn try_alloc_with_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_with(|| SmartPtrOverAligned(0)).unwrap_err(); +} + +#[test] +fn try_alloc_arc_with_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_arc_with(|| SmartPtrOverAlignedDrop(0)).unwrap_err(); +} + +#[test] +fn try_alloc_box_with_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_box_with(|| SmartPtrOverAlignedDrop(0)).unwrap_err(); +} + +#[test] +fn try_alloc_uninit_box_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_uninit_box::().unwrap_err(); +} + +#[test] +fn try_alloc_uninit_arc_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_uninit_arc::().unwrap_err(); +} + +#[test] +fn try_alloc_slice_copy_arc_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + let src = [SmartPtrOverAligned(0), SmartPtrOverAligned(1)]; + arena.try_alloc_slice_copy_arc(&src[..]).unwrap_err(); +} + +#[test] +fn try_alloc_arc_with_no_drop_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_arc_with(|| SmartPtrOverAligned(0)).unwrap_err(); +} + +#[test] +fn try_alloc_slice_fill_with_arc_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_slice_fill_with_arc(1, |_| SmartPtrOverAlignedDrop(0)).unwrap_err(); +} + +#[test] +fn try_alloc_slice_fill_with_arc_no_drop_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_slice_fill_with_arc(2, |_| SmartPtrOverAligned(0)).unwrap_err(); +} + +#[test] +fn try_alloc_slice_fill_with_box_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_slice_fill_with_box(1, |_| SmartPtrOverAlignedDrop(0)).unwrap_err(); +} + +#[test] +fn try_alloc_uninit_slice_arc_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_uninit_slice_arc::(1).unwrap_err(); +} + +#[test] +fn try_alloc_uninit_slice_box_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_uninit_slice_box::(1).unwrap_err(); +} + +// A pinned box hands out a smart pointer, so it rejects at the smart-pointer +// cap like the other box entry points. +#[test] +fn try_alloc_uninit_box_pin_rejects_smart_ptr_alignment() { + let arena = capped_arena(); + arena.try_alloc_uninit_box_pin::().unwrap_err(); +} + +// Simple-reference slices hand back a plain `&mut [T]` with no header +// recovery, so they use the looser chunk cap and only reject at it. +#[test] +fn try_alloc_slice_fill_with_rejects_chunk_alignment() { + let arena = capped_arena(); + arena.try_alloc_slice_fill_with(1, |_| ChunkOverAligned(0)).unwrap_err(); +} + +#[test] +fn try_alloc_slice_copy_rejects_chunk_alignment() { + let arena = capped_arena(); + let src = [ChunkOverAligned(0)]; + arena.try_alloc_slice_copy(&src[..]).unwrap_err(); +} + +#[test] +fn try_alloc_slice_fill_iter_rejects_chunk_alignment() { + let arena = capped_arena(); + arena.try_alloc_slice_fill_iter((0..1).map(|_| ChunkOverAligned(0))).unwrap_err(); +} + +// A type aligned below the chunk cap is accepted by the simple-reference +// slice paths: the boundary the tighter smart-pointer cap does not apply to. +#[test] +fn alloc_slice_ref_accepts_below_chunk_alignment_for_non_drop() { + let arena = capped_arena(); + let filled = arena.alloc_slice_fill_with(1, |_| SmartPtrOverAligned(0)); + assert_eq!(filled.len(), 1); + + let src = [SmartPtrOverAligned(0)]; + let cloned = arena.alloc_slice_clone(&src[..]); + assert_eq!(cloned.len(), 1); +} + +// Panicking entry points route through the same guards and surface the +// rejection as the arena's allocation-failure panic. +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_with_panics_on_over_alignment() { + let arena = capped_arena(); + let _ = arena.alloc_with(|| SmartPtrOverAligned(0)); +} + +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_arc_with_panics_on_over_alignment() { + let arena = capped_arena(); + let _ = arena.alloc_arc_with(|| SmartPtrOverAligned(0)); +} + +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_box_with_panics_on_over_alignment() { + let arena = capped_arena(); + let _ = arena.alloc_box_with(|| SmartPtrOverAligned(0)); +} + +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_uninit_box_panics_on_over_alignment() { + let arena = capped_arena(); + let _ = arena.alloc_uninit_box::(); +} + +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_uninit_arc_panics_on_over_alignment() { + let arena = capped_arena(); + let _ = arena.alloc_uninit_arc::(); +} + +#[test] +#[should_panic(expected = "multitude: allocator returned AllocError")] +fn alloc_slice_copy_panics_on_over_alignment() { + let arena = capped_arena(); + let src = [ChunkOverAligned(0)]; + let _ = arena.alloc_slice_copy(&src[..]); +} diff --git a/crates/multitude/src/arena/alloc_slice_arc.rs b/crates/multitude/src/arena/alloc_slice_arc.rs index e177120ee..763d1f50b 100644 --- a/crates/multitude/src/arena/alloc_slice_arc.rs +++ b/crates/multitude/src/arena/alloc_slice_arc.rs @@ -12,7 +12,7 @@ use core::ptr::NonNull; use allocator_api2::alloc::Allocator; use super::alloc_prefixed::worst_case_strong_slice_payload; -use super::alloc_value::{MAX_SMART_PTR_ALIGN, acquire_chunk_ref}; +use super::alloc_value::acquire_chunk_ref; use super::{Arena, ExpectAlloc}; use crate::AllocError; use crate::arc::Arc; @@ -419,7 +419,7 @@ impl Arena { /// [`Self::impl_alloc_slice_smart_copy`]. #[inline] fn alloc_slice_smart_copy_raw(&self, src: &[T]) -> Result, AllocError> { - check_slice_arc_layout::()?; + check_slice_arc_layout::(self)?; let len = src.len(); // `src` is a live `&[T]`, so `size_of_val(src)` is a valid `usize`. let payload_bytes = mem::size_of_val(src); @@ -488,7 +488,7 @@ impl Arena { /// [`Self::impl_alloc_slice_smart_with`]. #[inline] fn alloc_slice_smart_with_raw T>(&self, len: usize, f: F) -> Result, AllocError> { - check_slice_arc_layout::()?; + check_slice_arc_layout::(self)?; if let Some((uninit, chunk_ptr)) = self.try_reserve_arc_slice::(len) { let chunk_ref = self.acquire_current_chunk_ref(chunk_ptr); let slice_ptr = uninit.init_with_ptr(f); @@ -644,8 +644,8 @@ impl Arena { /// Up-front check for the `Arc<[T]>` / `Rc<[T]>` slice family. Rejects /// over-aligned `T` (would break the smart-pointer header recovery). #[inline] -fn check_slice_arc_layout() -> Result<(), AllocError> { - if mem::align_of::() >= MAX_SMART_PTR_ALIGN { +fn check_slice_arc_layout(arena: &Arena) -> Result<(), AllocError> { + if arena.rejects_smart_ptr_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } Ok(()) diff --git a/crates/multitude/src/arena/alloc_slice_box.rs b/crates/multitude/src/arena/alloc_slice_box.rs index bdd253e29..2b9e48325 100644 --- a/crates/multitude/src/arena/alloc_slice_box.rs +++ b/crates/multitude/src/arena/alloc_slice_box.rs @@ -12,7 +12,7 @@ use core::ptr::{self, NonNull}; use allocator_api2::alloc::Allocator; use super::alloc_prefixed::worst_case_thin_slice_payload; -use super::alloc_value::{MAX_SMART_PTR_ALIGN, acquire_chunk_ref}; +use super::alloc_value::acquire_chunk_ref; use super::{Arena, ExpectAlloc}; use crate::AllocError; use crate::r#box::Box; @@ -229,7 +229,7 @@ impl Arena { /// Box: `Box::drop` runs `drop_in_place` on the slice eagerly. Copy fast path. #[inline] fn impl_alloc_slice_box_copy(&self, src: &[T]) -> Result, AllocError> { - check_slice_box_layout::(src.len())?; + check_slice_box_layout::(self, src.len())?; let len = src.len(); // Precompute byte size so the reservation helper skips checked_mul. let payload_bytes = mem::size_of_val(src); @@ -248,7 +248,7 @@ impl Arena { #[inline] #[cfg_attr(test, mutants::skip)] // `+= → *=` on the fill counter ⇒ infinite loop fn impl_alloc_slice_box_with T>(&self, len: usize, mut f: F) -> Result, AllocError> { - check_slice_box_layout::(len)?; + check_slice_box_layout::(self, len)?; // Check overflow before the refill loop. let payload_bytes = mem::size_of::().checked_mul(len).ok_or(AllocError::CAPACITY_OVERFLOW)?; let ptr = self.reserve_slice_box::(len, payload_bytes, |slot_ptr| { @@ -379,8 +379,8 @@ impl Arena { /// smart-pointer header recovery. Slice length is full-width in the /// chunk prefix. #[inline] -fn check_slice_box_layout(_len: usize) -> Result<(), AllocError> { - if mem::align_of::() >= MAX_SMART_PTR_ALIGN { +fn check_slice_box_layout(arena: &Arena, _len: usize) -> Result<(), AllocError> { + if arena.rejects_smart_ptr_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } Ok(()) diff --git a/crates/multitude/src/arena/alloc_slice_ref.rs b/crates/multitude/src/arena/alloc_slice_ref.rs index 42327a515..42bb43b5d 100644 --- a/crates/multitude/src/arena/alloc_slice_ref.rs +++ b/crates/multitude/src/arena/alloc_slice_ref.rs @@ -19,17 +19,16 @@ use core::ptr::NonNull; use allocator_api2::alloc::Allocator; use super::{Arena, ExpectAlloc}; -use crate::internal::constants::CHUNK_ALIGN; use crate::{Alloc, AllocError}; /// Reject over-aligned slice element types early. Simple-reference /// slices return a plain `&mut [T]` (no header-recovery mask), so they /// can use the full chunk and only reject alignments that no single -/// chunk could satisfy (`>= CHUNK_ALIGN`). This is a looser cap than the -/// smart-pointer slice paths, which need [`MAX_SMART_PTR_ALIGN`]. +/// chunk could satisfy. This is a looser cap than the smart-pointer +/// slice paths, which need [`Arena::smart_ptr_align_cap`]. #[inline(always)] -fn reject_over_aligned() -> Result<(), AllocError> { - if const { mem::align_of::() >= CHUNK_ALIGN } { +fn reject_over_aligned(arena: &Arena) -> Result<(), AllocError> { + if arena.rejects_chunk_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } Ok(()) @@ -306,7 +305,7 @@ impl Arena { /// `try_alloc_slice_copy`. `T: Copy` requires no drop entry. #[inline(always)] fn alloc_slice_copy_raw(&self, src: &[T]) -> Result<&mut [T], AllocError> { - reject_over_aligned::()?; + reject_over_aligned::(self)?; let len = src.len(); if len == 0 { return Ok(empty_slice::()); @@ -373,7 +372,7 @@ impl Arena { /// `try_alloc_slice_clone`. `PANIC` monomorphizes the error arm. #[inline(always)] fn alloc_slice_clone_raw(&self, src: &[T]) -> Result<&mut [T], AllocError> { - reject_over_aligned::()?; + reject_over_aligned::(self)?; let len = src.len(); if len == 0 { return Ok(empty_slice::()); @@ -436,7 +435,7 @@ impl Arena { /// line avoids materializing closure state on the common path. #[inline(always)] fn alloc_slice_fill_with_raw T>(&self, len: usize, f: F) -> Result<&mut [T], AllocError> { - reject_over_aligned::()?; + reject_over_aligned::(self)?; if len == 0 { return Ok(empty_slice::()); } @@ -485,7 +484,7 @@ impl Arena { /// arena slot from `iter` and takes ownership of it in an [`Alloc`]. #[inline(always)] fn impl_alloc_slice_fill_iter>(&self, iter: I) -> Result, AllocError> { - reject_over_aligned::()?; + reject_over_aligned::(self)?; let len = iter.len(); let slot = self.alloc_slice_fill_iter_raw::(len, iter)?; // SAFETY: `alloc_slice_fill_iter_raw` initialized exactly `len` diff --git a/crates/multitude/src/arena/alloc_unsized.rs b/crates/multitude/src/arena/alloc_unsized.rs index a49479741..ed2026a1b 100644 --- a/crates/multitude/src/arena/alloc_unsized.rs +++ b/crates/multitude/src/arena/alloc_unsized.rs @@ -19,17 +19,10 @@ use super::alloc_value::acquire_chunk_ref; use super::{Arena, ExpectAlloc}; use crate::arc::Arc; use crate::r#box::Box; -use crate::internal::constants::max_smart_ptr_align; use crate::internal::thin_dst::{AtomicStrong, LocalStrong, Strong, strong_prefix_bytes_for}; use crate::rc::Rc; use crate::{AllocError, SmartPointerPointee}; -/// Maximum `layout.align()` accepted by smart-pointer allocations. -/// Mirrors the constant of the same name in [`alloc_value`](super::alloc_value): -/// values must lie strictly inside the first `CHUNK_ALIGN` bytes of -/// their chunk so the header-recovery mask works. -const MAX_SMART_PTR_ALIGN: usize = max_smart_ptr_align(); - impl Arena { /// Allocate a possibly-unsized `T` and return an `Arc`. /// @@ -394,7 +387,7 @@ impl Arena { metadata: T::Metadata, init: impl FnOnce(*mut T), ) -> Result, AllocError> { - if layout.align() >= MAX_SMART_PTR_ALIGN { + if self.rejects_smart_ptr_align(layout.align()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } let meta_bytes = T::ALLOCATION_METADATA_BYTES; @@ -483,7 +476,7 @@ impl Arena { metadata: T::Metadata, init: impl FnOnce(*mut T), ) -> Result, AllocError> { - if layout.align() >= MAX_SMART_PTR_ALIGN { + if self.rejects_smart_ptr_align(layout.align()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } let meta_bytes = T::ALLOCATION_METADATA_BYTES; diff --git a/crates/multitude/src/arena/alloc_value.rs b/crates/multitude/src/arena/alloc_value.rs index e4c61422e..3f481d0ee 100644 --- a/crates/multitude/src/arena/alloc_value.rs +++ b/crates/multitude/src/arena/alloc_value.rs @@ -15,7 +15,6 @@ use super::{Arena, ExpectAlloc}; use crate::arc::Arc; use crate::r#box::Box; use crate::internal::chunk_ref::ChunkRef; -use crate::internal::constants::max_smart_ptr_align; use crate::internal::thin_dst::{AtomicStrong, LocalStrong}; use crate::internal::uninit::Uninit; use crate::internal::{Chunk, thin_dst}; @@ -47,16 +46,6 @@ fn worst_case_strong_payload() -> usize { .saturating_add(S::block_align(align)) } -/// Maximum `align_of::()` accepted by smart-pointer allocations. -/// -/// Boxes recover their chunk header by subtracting the value pointer's -/// offset within its `CHUNK_ALIGN` tile; for that step to land on the -/// header rather than the value itself, the value must lie strictly -/// inside the first `CHUNK_ALIGN` bytes. Keeping the alignment well -/// below `CHUNK_ALIGN` leaves room for the chunk header plus the -/// value itself in the dedicated oversized case. -pub(in crate::arena) const MAX_SMART_PTR_ALIGN: usize = max_smart_ptr_align(); - impl Arena { /// Allocate `value` and return a `Send + Sync` reference-counted smart pointer. /// @@ -807,7 +796,7 @@ impl Arena { /// [`Self::impl_alloc_value_with`]. #[inline(always)] fn alloc_value_with_raw T>(&self, f: F) -> Result<&mut T, AllocError> { - if const { mem::align_of::() >= MAX_SMART_PTR_ALIGN } { + if self.rejects_smart_ptr_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } // `f` is moved into exactly one of the in-chunk or fallback paths. @@ -897,7 +886,7 @@ impl Arena { #[inline(always)] #[cfg_attr(test, mutants::skip)] // routing-predicate mutations ⇒ refill spin (OOM) fn alloc_smart_prefixed_with_raw T>(&self, f: F) -> Result, AllocError> { - if const { mem::align_of::() >= MAX_SMART_PTR_ALIGN } { + if self.rejects_smart_ptr_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } let mut f = Some(f); @@ -924,14 +913,14 @@ impl Arena { /// into the reservation. [`Box`] runs `T::drop` eagerly in its own /// `Drop`. /// - /// Rejects alignments at or above [`MAX_SMART_PTR_ALIGN`]: such + /// Rejects alignments at or above [`Arena::smart_ptr_align_cap`]: such /// values cannot live inside the first [`CHUNK_ALIGN`] bytes of a /// chunk, which would break the header-recovery mask used by the /// smart pointers' `Drop` impls. #[inline(always)] #[cfg_attr(test, mutants::skip)] // routing-predicate mutations ⇒ refill spin (OOM) fn impl_alloc_smart_with T>(&self, f: F) -> Result, AllocError> { - if const { mem::align_of::() >= MAX_SMART_PTR_ALIGN } { + if self.rejects_smart_ptr_align(mem::align_of::()) { return Err(AllocError::ALIGNMENT_TOO_LARGE); } loop { diff --git a/crates/multitude/src/arena/mod.rs b/crates/multitude/src/arena/mod.rs index d1f12369d..e7e8606fc 100644 --- a/crates/multitude/src/arena/mod.rs +++ b/crates/multitude/src/arena/mod.rs @@ -18,7 +18,7 @@ use crate::internal::chunk::Chunk; use crate::internal::chunk_mutator::ChunkMutator; use crate::internal::chunk_provider::{ChunkProvider, ChunkProviderConfig}; use crate::internal::chunk_ref::ChunkRef; -use crate::internal::constants::{MAX_NORMAL_ALLOC, SizeClass}; +use crate::internal::constants::{CHUNK_ALIGN, MAX_NORMAL_ALLOC, SizeClass}; use crate::internal::current_chunk::CurrentChunk; /// Surplus of chunk strong refs the arena pre-credits to the @@ -40,6 +40,8 @@ use crate::internal::current_chunk::CurrentChunk; /// chunk retire. const LARGE_SHARED_REF_SURPLUS: u32 = 1 << 30; +#[cfg(test)] +mod align_guard_tests; mod alloc_growable; #[cfg(feature = "hashbrown")] mod alloc_hashbrown; @@ -152,6 +154,12 @@ pub struct Arena { /// Number of completed bulk resets. #[cfg(feature = "stats")] resets: Cell, + + /// Test-only override for [`CHUNK_ALIGN`] in the alignment-rejection + /// guards. Production builds read the constant directly; see + /// [`Self::chunk_align_cap`]. + #[cfg(test)] + align_cap: Cell, } // Fields make `Arena` sendable when `A: Send + Sync`, but `CurrentChunk` and @@ -315,6 +323,8 @@ impl Arena { relocations_since_reset: Cell::new(0), #[cfg(feature = "stats")] resets: Cell::new(0), + #[cfg(test)] + align_cap: Cell::new(CHUNK_ALIGN), }) } @@ -551,6 +561,72 @@ impl Arena { self.provider.config().max_normal_alloc() } + /// Exclusive upper bound on the alignment a chunk can satisfy. + /// + /// Production builds always report [`CHUNK_ALIGN`]. Tests lower it + /// with [`Self::set_align_cap`] so the rejection guards can be driven + /// by types whose alignment every codegen backend accepts — a type + /// aligned to the real cap does not compile everywhere. + #[cfg(not(test))] + #[inline(always)] + #[expect(clippy::unused_self, reason = "signature must match the cfg(test) arm, which reads a field")] + pub(crate) fn chunk_align_cap(&self) -> usize { + CHUNK_ALIGN + } + + #[cfg(test)] + #[inline(always)] + pub(crate) fn chunk_align_cap(&self) -> usize { + self.align_cap.get() + } + + /// Lower the alignment caps so tests can reach them with types the + /// compiler will accept. `cap` replaces [`CHUNK_ALIGN`]; the + /// smart-pointer cap stays at half of it, as in production. + /// + /// Only the allocation guards consult this. The `buffer_freezable` + /// predicate that decides whether a `Vec` can freeze in place still + /// compares against the real constant, so a capped arena is not a + /// faithful model for the growable-collection paths. + #[cfg(test)] + pub(crate) fn set_align_cap(&self, cap: usize) { + assert!(cap.is_power_of_two(), "alignment cap must be a power of two"); + assert!( + cap <= CHUNK_ALIGN, + "the cap may only be lowered: raising it would let the guards accept alignments no chunk can satisfy" + ); + assert!(cap >= 4, "alignment cap must leave room for a non-zero smart-pointer cap"); + self.align_cap.set(cap); + } + + /// Exclusive upper bound on the alignment a smart-pointer allocation + /// can satisfy. + /// + /// Values at or above this cap can no longer be guaranteed to lie + /// strictly inside the first [`CHUNK_ALIGN`] bytes of their chunk, + /// which would break the header-recovery mask used by `Drop` and + /// `deallocate`. + #[inline(always)] + pub(crate) fn smart_ptr_align_cap(&self) -> usize { + self.chunk_align_cap() / 2 + } + + /// Whether `align` is too large for a smart-pointer allocation. + #[inline(always)] + pub(crate) fn rejects_smart_ptr_align(&self, align: usize) -> bool { + align >= self.smart_ptr_align_cap() + } + + /// Whether `align` exceeds what any single chunk can satisfy. + /// + /// A looser cap than [`Self::rejects_smart_ptr_align`]: simple-reference + /// allocations hand back a plain reference with no header-recovery mask, + /// so they can use the full chunk. + #[inline(always)] + pub(crate) fn rejects_chunk_align(&self, align: usize) -> bool { + align >= self.chunk_align_cap() + } + /// True iff an allocation request of `min_payload` bytes must be routed /// to a one-shot oversized chunk instead of the normal size-class pool. /// Callers that detect this case should use the matching oversized path diff --git a/crates/multitude/src/bytemuck.rs b/crates/multitude/src/bytemuck.rs index 53f8e5e6e..a477758f4 100644 --- a/crates/multitude/src/bytemuck.rs +++ b/crates/multitude/src/bytemuck.rs @@ -450,3 +450,94 @@ impl<'a, A: Allocator + Clone> BytemuckView<'a, A> { self.arena.try_alloc_slice_fill_with_box::(len, |_| T::zeroed()) } } + +#[cfg(test)] +mod tests { + //! Every `BytemuckView` entry point rejects a type aligned at or above + //! the relevant cap. The smart-pointer paths and the single-value + //! reference paths reject at the smart-pointer cap; the reference-slice + //! paths reject only at the looser chunk cap. + //! + //! The tests run against [`capped_arena`], whose caps are lowered so the + //! boundary is reachable by an alignment every codegen backend accepts. + + use crate::tests_support::{ChunkOverAligned, SmartPtrOverAligned, capped_arena}; + + #[test] + fn try_alloc_box_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc_box::().unwrap_err(); + } + + #[test] + fn try_alloc_arc_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc_arc::().unwrap_err(); + } + + #[test] + fn try_alloc_slice_box_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc_slice_box::(4).unwrap_err(); + } + + #[test] + fn try_alloc_slice_arc_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc_slice_arc::(4).unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_box_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc_box::(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_arc_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc_arc::(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_box_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc_slice_box::(4); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_arc_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc_slice_arc::(4); + } + + #[test] + fn try_alloc_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc::().unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc::(); + } + + #[test] + fn try_alloc_slice_over_aligned_returns_err() { + let arena = capped_arena(); + arena.bytemuck().try_alloc_slice::(4).unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.bytemuck().alloc_slice::(4); + } +} diff --git a/crates/multitude/src/error.rs b/crates/multitude/src/error.rs index e8710d159..764383bfe 100644 --- a/crates/multitude/src/error.rs +++ b/crates/multitude/src/error.rs @@ -91,19 +91,17 @@ impl AllocError { /// Report whether the request exceeded the arena's supported alignment. /// - /// Such a request can never - /// succeed, regardless of how much memory is available. + /// Such a request can never succeed, regardless of how much memory is + /// available. There are two boundaries. Smart pointers and single values + /// are rejected at half the chunk alignment, because a smart pointer + /// recovers its chunk header by masking the value pointer and a value + /// aligned that far can fall outside the chunk's first tile. + /// Simple-reference slices hand back a plain reference with no header + /// recovery, so they are rejected only at the full chunk alignment. /// - /// ``` - /// #[repr(align(32768))] - /// struct OverAligned; - /// - /// let arena = multitude::Arena::new(); - /// let Some(error) = arena.try_alloc(OverAligned).err() else { - /// panic!("over-aligned values must be rejected"); - /// }; - /// assert!(error.is_alignment_too_large()); - /// ``` + /// No example: naming a type aligned that far does not compile on every + /// supported codegen backend. The behaviour is covered by the arena's + /// alignment-guard unit tests. #[must_use] pub fn is_alignment_too_large(self) -> bool { matches!(self.kind, ErrorKind::AlignmentTooLarge) diff --git a/crates/multitude/src/tests_support.rs b/crates/multitude/src/tests_support.rs index 37d2fab3f..c2d7d9466 100644 --- a/crates/multitude/src/tests_support.rs +++ b/crates/multitude/src/tests_support.rs @@ -15,6 +15,69 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use allocator_api2::alloc::{AllocError, Allocator, Global}; +use crate::Arena; + +/// Chunk-alignment cap installed by [`capped_arena`]. +/// +/// The real cap is [`CHUNK_ALIGN`](crate::internal::constants::CHUNK_ALIGN), +/// 64 KiB, and the smart-pointer cap is half of that. No test can name a +/// type aligned that far — codegen backends differ in the maximum type +/// alignment they accept, and 32 KiB is above the lowest of them. Lowering +/// the caps puts both boundaries within reach of an alignment every backend +/// compiles, so the rejection guards can be driven everywhere. +pub(crate) const TEST_CHUNK_ALIGN: usize = 8192; + +/// Smart-pointer alignment cap under [`TEST_CHUNK_ALIGN`]. Half the chunk +/// cap, as in production. +pub(crate) const TEST_SMART_PTR_ALIGN: usize = TEST_CHUNK_ALIGN / 2; + +/// Arena whose alignment caps are low enough for a test to reach them. +/// +/// Do not use for `Vec` / `String` growth or freeze tests. `buffer_freezable` +/// still compares against the real cap, so this arena classifies a +/// `TEST_SMART_PTR_ALIGN`-aligned element as freezable while the allocation +/// guards reject it — the two disagree only here, never in production. +pub(crate) fn capped_arena() -> Arena { + let arena = Arena::new(); + arena.set_align_cap(TEST_CHUNK_ALIGN); + arena +} + +/// Aligned exactly at the smart-pointer cap: rejected by every +/// smart-pointer entry point, accepted by the simple-reference slice paths. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Zeroable))] +#[cfg_attr(feature = "zerocopy", derive(zerocopy::FromZeros))] +#[repr(C, align(4096))] +pub(crate) struct SmartPtrOverAligned(pub(crate) u8); + +/// [`SmartPtrOverAligned`] with a destructor, for the paths that branch on +/// `needs_drop::()`. +#[derive(Debug)] +#[repr(C, align(4096))] +pub(crate) struct SmartPtrOverAlignedDrop(pub(crate) u8); + +#[expect(clippy::empty_drop, reason = "the impl exists to make needs_drop::() true")] +impl Drop for SmartPtrOverAlignedDrop { + fn drop(&mut self) {} +} + +/// Aligned exactly at the chunk cap: no chunk can satisfy it, so even the +/// simple-reference paths reject it. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "bytemuck", derive(bytemuck::Zeroable))] +#[cfg_attr(feature = "zerocopy", derive(zerocopy::FromZeros))] +#[repr(C, align(8192))] +pub(crate) struct ChunkOverAligned(pub(crate) u8); + +// `repr(align)` takes a literal, so the helper types cannot name the cap +// constants directly. Keep them in step. +const _: () = { + assert!(align_of::() == TEST_SMART_PTR_ALIGN); + assert!(align_of::() == TEST_SMART_PTR_ALIGN); + assert!(align_of::() == TEST_CHUNK_ALIGN); +}; + /// Send-and-Sync allocator that fails after a fixed number of successful /// allocations. #[derive(Clone)] diff --git a/crates/multitude/src/zerocopy.rs b/crates/multitude/src/zerocopy.rs index 26f2d742c..ccb724615 100644 --- a/crates/multitude/src/zerocopy.rs +++ b/crates/multitude/src/zerocopy.rs @@ -447,3 +447,88 @@ impl<'a, A: Allocator + Clone> ZerocopyView<'a, A> { self.arena.try_alloc_slice_fill_with_box::(len, |_| T::new_zeroed()) } } + +#[cfg(test)] +mod tests { + use crate::tests_support::{ChunkOverAligned, SmartPtrOverAligned, capped_arena}; + + #[test] + fn try_alloc_box_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc_box::().unwrap_err(); + } + + #[test] + fn try_alloc_arc_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc_arc::().unwrap_err(); + } + + #[test] + fn try_alloc_slice_box_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc_slice_box::(4).unwrap_err(); + } + + #[test] + fn try_alloc_slice_arc_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc_slice_arc::(4).unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_box_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc_box::(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_arc_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc_arc::(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_box_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc_slice_box::(4); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_arc_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc_slice_arc::(4); + } + + // The scalar entry points forward to `Arena::try_alloc_with`, which uses + // the smart-pointer cap — not the looser chunk cap the slice paths use. + #[test] + fn try_alloc_ref_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc::().unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_ref_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc::(); + } + + #[test] + fn try_alloc_slice_ref_over_aligned_returns_err() { + let arena = capped_arena(); + arena.zerocopy().try_alloc_slice::(4).unwrap_err(); + } + + #[test] + #[should_panic = "arena allocation failed"] + fn alloc_slice_ref_panics_on_over_aligned() { + let arena = capped_arena(); + let _ = arena.zerocopy().alloc_slice::(4); + } +} diff --git a/crates/multitude/tests/arena.rs b/crates/multitude/tests/arena.rs index 789964a36..a406f861c 100644 --- a/crates/multitude/tests/arena.rs +++ b/crates/multitude/tests/arena.rs @@ -2226,28 +2226,6 @@ mod coverage_arena_gaps { #[cfg(feature = "std")] use crate::common; - // ============================================================================ - // Helpers - // ============================================================================ - - /// Half-chunk-aligned (`MAX_SMART_PTR_ALIGN`) type without `Drop`. - /// Used to drive over-alignment rejection in `_with` family functions - /// without ever instantiating the value on the test stack frame - /// (the over-alignment guard fires before the closure is invoked). - #[cfg(not(align_capped_backend))] - #[repr(align(32768))] - #[derive(Clone, Copy)] - struct HalfChunkAlign; - - /// Chunk-aligned (`CHUNK_ALIGN`) Copy type used to drive the - /// `layout.align() >= CHUNK_ALIGN` guard in the slice-copy family. - /// Same Windows-stack caveat as [`HalfChunkAlign`]: never lives on - /// the test stack. - #[cfg(not(align_capped_backend))] - #[repr(align(65536))] - #[derive(Clone, Copy)] - struct ChunkAlign; - #[test] fn try_alloc_simple_ref_returns_mutable_reference() { let arena = Arena::::new(); @@ -2297,14 +2275,6 @@ mod coverage_arena_gaps { assert_eq!(arc[69_999], 7); } - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_arc_with_over_aligned_panics() { - let arena = Arena::::new(); - let _ = arena.alloc_arc_with::(|| HalfChunkAlign); - } - #[test] fn try_alloc_with_oversized_value_succeeds() { let arena = Arena::::new(); @@ -2313,83 +2283,6 @@ mod coverage_arena_gaps { assert_eq!(r[69_999], 3); } - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_with_over_aligned_panics() { - let arena = Arena::::new(); - let _ = arena.alloc_with(|| HalfChunkAlign); - } - - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_box_with_over_aligned_panics() { - let arena = Arena::::new(); - let _ = arena.alloc_box_with::(|| HalfChunkAlign); - } - - // Over-alignment is rejected before initialization. - - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_uninit_box_over_aligned_panics() { - let arena = Arena::::new(); - let _ = arena.alloc_uninit_box::(); - } - - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_uninit_arc_over_aligned_panics() { - let arena = Arena::::new(); - let _ = arena.alloc_uninit_arc::(); - } - - #[cfg(not(align_capped_backend))] - #[test] - #[should_panic(expected = "multitude: allocator returned AllocError")] - fn alloc_slice_copy_over_aligned_panics() { - let arena = Arena::::new(); - // Empty slice of a `CHUNK_ALIGN`-aligned `Copy` type triggers the - // `layout.align() >= CHUNK_ALIGN` guard at the top of - // `alloc_slice_local_copy_or_panic` without instantiating a value. - let src: &[ChunkAlign] = &[]; - let _ = arena.alloc_slice_copy(src); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_slice_no_drop_over_aligned_returns_err() { - let arena = Arena::::new(); - // `try_alloc_slice_fill_with` routes through - // `try_alloc_slice_local_no_drop_with` for `!needs_drop` T. The cap - // for the reference is `CHUNK_ALIGN` (the chunk-recovery - // limit), not the smart-pointer cap — so use a 64 KiB-aligned - // type to drive the rejection. - let res = arena.try_alloc_slice_fill_with::(1, |_| ChunkAlign); - assert!(res.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_slice_copy_over_aligned_returns_err() { - let arena = Arena::::new(); - let src: &[ChunkAlign] = &[]; - let res = arena.try_alloc_slice_copy(src); - assert!(res.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_slice_copy_arc_over_aligned_returns_err() { - let arena = Arena::::new(); - let src: &[ChunkAlign] = &[]; - let res = arena.try_alloc_slice_copy_arc(src); - assert!(res.is_err()); - } - // Per-value reference counting permits drop slices longer than `u16::MAX`. #[cfg(all(feature = "std", not(miri)))] @@ -2449,14 +2342,6 @@ mod coverage_arena_gaps { assert_eq!(arc[2047], 2047); } - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_arc_with_over_aligned_returns_err() { - let arena = Arena::::new(); - let res = arena.try_alloc_arc_with::(|| HalfChunkAlign); - assert!(res.is_err()); - } - #[cfg(feature = "std")] #[test] fn alloc_with_closure_induced_eviction_commits_drop_entry() { @@ -3910,6 +3795,11 @@ mod refactor_coverage_gaps { // `try_reserve_local_slice`, and the non-freezable oversized arm. An // over-aligned element (align ≥ max_smart_ptr_align == CHUNK_ALIGN/2) is // non-freezable, and at 32 KiB each it forces the oversized refill. + // + // This is the last type in the crate aligned above 8192, which some + // codegen backends refuse to compile. It builds because the test never + // constructs an `Over`: `try_reserve` computes the layout and reserves + // aligned storage, but no typed value is materialized. #[test] fn non_freezable_overaligned_vec_grows_via_oversized_path() { #[repr(align(32768))] @@ -6079,32 +5969,6 @@ mod public_surface_behavior { assert!(r.is_err()); } - // `#[repr(align(N))]` with N > CHUNK_ALIGN (64 KiB). Used by the two - // tests below to drive the `if layout.align() > CHUNK_ALIGN { return - // Err(AllocError) }` guard in `try_alloc_with` and `try_reserve_and_init`. - // - // The guard lives in a thin outer function whose frame doesn't depend - // on `T`'s alignment, so the test runs on every LLVM-backed platform — - // including Windows, whose default 1 MiB stack can't accommodate the - // 128 KiB-aligned frame the guarded body would otherwise require. - // - // Skipped on codegen backends that cap type alignment below the 128 KiB - // this test needs (`--cfg align_capped_backend`). - #[cfg(not(align_capped_backend))] - #[repr(align(131072))] - struct HugeAlign(#[expect(dead_code, reason = "field present to give the type a non-zero size")] u8); - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_with_rejects_excessive_alignment() { - // try_alloc_with is the Alloc entry point. CHUNK_ALIGN is 64 KiB; - // HugeAlign needs 128 KiB alignment, so the layout-align check - // must fire and return Err. - let arena: Arena = Arena::new(); - let result: Result, _> = arena.try_alloc_with(|| HugeAlign(0)); - assert!(result.is_err()); - } - #[test] fn try_alloc_string_with_capacity_huge_returns_err() { let arena: Arena = Arena::new(); @@ -6339,23 +6203,6 @@ mod public_surface_behavior { .build(); } - // Distinct type from `HugeAlign` above so we don't perturb the caller's frame - // alignment and trigger the issue noted in the comment near - // `try_alloc_with_rejects_excessive_alignment`. The `MaybeUninit` returned - // by the uninit-family entry points never materializes a real `T` on the - // stack, so the test compiles and runs safely on every platform. - #[cfg(not(align_capped_backend))] - #[repr(align(131072))] - struct HugeAlignBox(#[expect(dead_code, reason = "field gives the type a non-zero size")] u8); - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_uninit_box_rejects_excessive_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_uninit_box::(); - assert!(r.is_err()); - } - #[test] fn arena_string_replace_range_excluded_start() { use core::ops::Bound; @@ -6792,22 +6639,6 @@ mod public_surface_behavior { assert!(s.len() >= n); } - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - // See note on `acquire_slice_slot_rejects_overaligned`: naming a - // `T` with `align(131072)` aborts on Windows before the guard runs. - fn try_alloc_slice_copy_rejects_overaligned() { - #[repr(align(131072))] - #[derive(Clone, Copy)] - #[expect(dead_code, reason = "field needed for alignment/size but not read")] - struct HugeAlign(u8); - - let arena = Arena::new(); - let data = [HugeAlign(0)]; - let result = arena.try_alloc_slice_copy(&data[..]); - assert!(result.is_err()); - } - #[test] fn try_alloc_slice_copy_rejects_overflow() { let arena = Arena::new(); @@ -6815,23 +6646,6 @@ mod public_surface_behavior { assert!(result.is_err()); } - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - // See note on `acquire_slice_slot_rejects_overaligned`: naming a - // `T` with `align(131072)` aborts on Windows before the guard runs. - fn try_alloc_slice_fill_with_rejects_overaligned() { - #[repr(align(131072))] - struct HugeAlignDrop(#[expect(dead_code, reason = "field needed for alignment/size but not read")] u8); - #[expect(clippy::empty_drop, reason = "Drop impl makes needs_drop::() true for test")] - impl Drop for HugeAlignDrop { - fn drop(&mut self) {} - } - - let arena = Arena::new(); - let result = arena.try_alloc_slice_fill_with::(1, |_| HugeAlignDrop(0)); - assert!(result.is_err()); - } - #[test] fn try_alloc_slice_fill_with_no_drop_fast_path() { let arena = Arena::new(); @@ -6904,115 +6718,6 @@ mod public_surface_behavior { assert_eq!(&*s, &[1, 2, 3, 4, 5, 6, 7, 8]); } - // - // All smart-pointer alloc paths reject `align >= 32 KiB` because, with - // the co-allocated `DropEntry` taking 32 bytes immediately before the - // payload, an `align == 32 KiB` payload lands at chunk offset - // `CHUNK_ALIGN`. `header_for(value_ptr)` masks the low 16 bits of the - // pointer to recover the chunk header — for that offset, the mask - // returns the *next* chunk's address. The guard exists to make this - // failure mode unreachable from safe code. - // - // These tests pin the boundary: a sized `T` with `repr(align(32768))` - // must be rejected by every smart-pointer entry point. The companion - // tests in `dst.rs` cover the unsafe DST paths. - // - // Skipped on Windows: naming a type with `align(32768)` on stack inside - // `try_alloc_*_with` materializes a stack frame Windows' default 1 MiB - // stack cannot satisfy on entry, aborting with STATUS_STACK_OVERFLOW - // before the guard runs. The MaybeUninit/uninit-family tests only hold - // the type *inside* `MaybeUninit`, so they're safe everywhere. - - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - #[repr(align(32768))] - #[derive(Clone, Copy)] - struct HalfChunkAlignNoDrop(#[expect(dead_code, reason = "field gives the type a non-zero size")] u8); - - #[cfg(not(align_capped_backend))] - #[repr(align(32768))] - struct HalfChunkAlignDrop(#[expect(dead_code, reason = "field gives the type a non-zero size")] u8); - - #[cfg(not(align_capped_backend))] - #[expect(clippy::empty_drop, reason = "Drop impl makes needs_drop::() true for the test")] - impl Drop for HalfChunkAlignDrop { - fn drop(&mut self) {} - } - - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - fn try_alloc_arc_with_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r: Result, _> = arena.try_alloc_arc_with(|| HalfChunkAlignDrop(0)); - assert!(r.is_err()); - } - - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - fn try_alloc_box_with_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r: Result, _> = arena.try_alloc_box_with(|| HalfChunkAlignDrop(0)); - assert!(r.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_uninit_box_rejects_half_chunk_alignment() { - // Holding T inside MaybeUninit means no stack frame needs T's - // alignment, so this test is portable to Windows. - let arena: Arena = Arena::new(); - let r = arena.try_alloc_uninit_box::(); - assert!(r.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_uninit_arc_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_uninit_arc::(); - assert!(r.is_err()); - } - - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - fn try_alloc_slice_fill_with_arc_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_slice_fill_with_arc::(1, |_| HalfChunkAlignDrop(0)); - assert!(r.is_err()); - } - - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - fn try_alloc_slice_fill_with_box_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_slice_fill_with_box::(1, |_| HalfChunkAlignDrop(0)); - assert!(r.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_uninit_slice_arc_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_uninit_slice_arc::(1); - assert!(r.is_err()); - } - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_uninit_slice_box_rejects_half_chunk_alignment() { - let arena: Arena = Arena::new(); - let r = arena.try_alloc_uninit_slice_box::(1); - assert!(r.is_err()); - } - - #[test] - #[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] - fn try_alloc_slice_copy_arc_allows_half_chunk_align_for_copy_t() { - let arena: Arena = Arena::new(); - let data = [HalfChunkAlignNoDrop(0), HalfChunkAlignNoDrop(1)]; - let r = arena.try_alloc_slice_copy_arc(&data[..]); - assert!(r.is_err()); - } - // // Each `alloc_*_with` reserves a slot, takes a protective `+1` chunk // refcount, then runs the user-supplied `f`. If `f` panics, the @@ -7608,29 +7313,6 @@ mod public_surface_behavior_3 { assert_eq!(DROPPED.load(Ordering::Relaxed), 4); } - #[cfg(not(align_capped_backend))] - #[repr(align(32768))] - #[derive(Clone, Copy)] - struct OverAligned32K; - - // SAFETY: zero-sized POD; no drop. - #[cfg(not(align_capped_backend))] - unsafe impl Send for OverAligned32K {} - // SAFETY: zero-sized POD; no drop. - #[cfg(not(align_capped_backend))] - unsafe impl Sync for OverAligned32K {} - - #[cfg(not(align_capped_backend))] - #[test] - fn try_alloc_slice_fill_with_arc_rejects_over_aligned() { - let arena = Arena::new(); - // `try_alloc_slice_fill_with_arc` for `T: !needs_drop` routes through - // `try_alloc_slice_shared_no_drop_with`, which checks - // `align >= MAX_SMART_PTR_ALIGN` and errors. - let result = arena.try_alloc_slice_fill_with_arc::(2, |_| OverAligned32K); - assert!(result.is_err()); - } - #[test] fn cache_push_pop_contention_drives_cas_retries() { use std::sync::Barrier; diff --git a/crates/multitude/tests/audit_repro.rs b/crates/multitude/tests/audit_repro.rs index 4a39098b3..fdc44a5fe 100644 --- a/crates/multitude/tests/audit_repro.rs +++ b/crates/multitude/tests/audit_repro.rs @@ -152,25 +152,6 @@ fn arc_concurrent_assume_init_no_race() { ); } -/// Reference slices accept non-Drop types aligned below `CHUNK_ALIGN`. -/// -/// We use a ZST with `#[repr(align(32768))]` so the type's alignment -/// checks the cap without forcing a 32 KiB stack frame. -#[cfg(not(align_capped_backend))] -#[test] -fn alloc_slice_ref_accepts_half_chunk_alignment_for_non_drop() { - #[repr(align(32768))] - #[derive(Clone, Copy)] - struct Wide; - let arena = Arena::new(); - let s = arena.alloc_slice_fill_with::(1, |_| Wide); - assert_eq!(s.len(), 1); - - let src: &[Wide] = &[Wide]; - let c = arena.alloc_slice_clone::(src); - assert_eq!(c.len(), 1); -} - /// Each refcounted ZST handout reserves a distinct one-byte tag, bounding /// per-chunk handouts and preserving refcount surplus invariants. #[test] diff --git a/crates/multitude/tests/bytemuck_integration.rs b/crates/multitude/tests/bytemuck_integration.rs index 4e923029a..d84f39688 100644 --- a/crates/multitude/tests/bytemuck_integration.rs +++ b/crates/multitude/tests/bytemuck_integration.rs @@ -148,86 +148,6 @@ fn try_alloc_slice_arc_ok() { assert!(v.iter().all(|&x| x == 0)); } -// Manually impl Zeroable for an over-aligned type since derive -// requires Clone+Copy which don't affect alignment semantics. -// -// Windows cannot materialize a 64 KiB-aligned value in its default stack; -// non-slice variants check the same alignment guard there. -#[cfg(not(align_capped_backend))] -#[derive(Clone, Copy)] -#[repr(C, align(65536))] -struct OverAligned { - _data: u8, -} - -// SAFETY: all-zeros is a valid OverAligned (it's just a u8 + padding). -#[cfg(not(align_capped_backend))] -unsafe impl Zeroable for OverAligned {} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_box_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc_box::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_arc_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc_arc::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_slice_box_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc_slice_box::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_slice_arc_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc_slice_arc::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_box_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc_box::(); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_arc_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc_arc::(); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_box_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc_slice_box::(4); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_arc_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc_slice_arc::(4); -} - #[test] fn bytemuck_view_debug() { let arena = Arena::new(); @@ -257,22 +177,6 @@ fn try_alloc_ref_scalar_ok() { assert_eq!(*v, 0); } -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_ref_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_ref_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc::(); -} - #[test] fn alloc_ref_is_mutable() { let arena = Arena::new(); @@ -303,22 +207,6 @@ fn try_alloc_slice_ref_ok() { assert_eq!(&*s, &[0, 0, 0]); } -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -fn try_alloc_slice_ref_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.bytemuck().try_alloc_slice::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_ref_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.bytemuck().alloc_slice::(4); -} - #[test] fn alloc_slice_ref_is_mutable() { let arena = Arena::new(); diff --git a/crates/multitude/tests/pin_support.rs b/crates/multitude/tests/pin_support.rs index c66ef4c7d..d9e46a3d7 100644 --- a/crates/multitude/tests/pin_support.rs +++ b/crates/multitude/tests/pin_support.rs @@ -68,18 +68,6 @@ fn alloc_arc_pin_with_cross_thread() { assert_eq!(addr_before, addr_thread, "Arc-pinned value must keep its address across threads"); } -#[cfg(not(align_capped_backend))] -#[test] -fn try_alloc_uninit_box_pin_rejects_over_alignment() { - // Avoid constructing the 32 KiB-aligned value on the stack. - #[repr(align(32768))] - #[expect(dead_code, reason = "drives the over-alignment guard before init runs")] - struct HalfChunk(u8); - let arena = Arena::new(); - let r: Result>>, _> = arena.try_alloc_uninit_box_pin::(); - r.unwrap_err(); -} - #[test] fn box_from_into_pin_value() { let arena = Arena::new(); diff --git a/crates/multitude/tests/zerocopy_integration.rs b/crates/multitude/tests/zerocopy_integration.rs index 30ad70efb..099b84217 100644 --- a/crates/multitude/tests/zerocopy_integration.rs +++ b/crates/multitude/tests/zerocopy_integration.rs @@ -121,79 +121,6 @@ fn try_alloc_slice_arc_ok() { assert!(v.iter().all(|&x| x == 0)); } -// Windows cannot materialize a 64 KiB-aligned value in its default stack; -// non-slice variants check the same alignment guard there. -#[cfg(not(align_capped_backend))] -#[derive(FromZeros)] -#[repr(C, align(65536))] -struct OverAligned { - _data: u8, -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_box_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc_box::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_arc_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc_arc::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_slice_box_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc_slice_box::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_slice_arc_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc_slice_arc::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_box_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc_box::(); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_arc_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc_arc::(); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_box_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc_slice_box::(4); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_arc_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc_slice_arc::(4); -} - #[test] fn zerocopy_view_debug() { let arena = Arena::new(); @@ -223,22 +150,6 @@ fn try_alloc_ref_scalar_ok() { assert_eq!(*v, 0); } -#[test] -#[cfg(not(align_capped_backend))] -fn try_alloc_ref_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc::(); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_ref_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc::(); -} - #[test] fn alloc_ref_is_mutable() { let arena = Arena::new(); @@ -269,22 +180,6 @@ fn try_alloc_slice_ref_ok() { assert_eq!(&*s, &[0, 0, 0]); } -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -fn try_alloc_slice_ref_over_aligned_returns_err() { - let arena = Arena::new(); - let result = arena.zerocopy().try_alloc_slice::(4); - assert!(result.is_err()); -} - -#[test] -#[cfg(all(not(target_os = "windows"), not(align_capped_backend)))] -#[should_panic = "arena allocation failed"] -fn alloc_slice_ref_panics_on_over_aligned() { - let arena = Arena::new(); - let _ = arena.zerocopy().alloc_slice::(4); -} - #[test] fn alloc_slice_ref_is_mutable() { let arena = Arena::new();