Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion crates/multitude/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
11 changes: 2 additions & 9 deletions crates/multitude/src/allocator_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<A>` is the allocator handle: cheap to copy and backed by
/// chunks. `allocate` bumps the chunk refcount; `deallocate`
Expand All @@ -34,7 +27,7 @@ unsafe impl<A: Allocator + Clone> Allocator for &Arena<A> {
// 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
Expand Down Expand Up @@ -270,7 +263,7 @@ mod tests {
unsafe { LegacyAllocator::deallocate(&handle, p.cast::<u8>(), 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");
}
}
246 changes: 246 additions & 0 deletions crates/multitude/src/arena/align_guard_tests.rs
Comment thread
AdomasBekeras marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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};
Comment thread
AdomasBekeras marked this conversation as resolved.

// 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);
Comment thread
AdomasBekeras marked this conversation as resolved.
}

#[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::<SmartPtrOverAligned>()));
assert!(arena.rejects_chunk_align(align_of::<ChunkOverAligned>()));

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::<u64>()));
assert!(!arena.rejects_chunk_align(align_of::<SmartPtrOverAligned>()));
}

#[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::<SmartPtrOverAlignedDrop>().unwrap_err();
}

#[test]
fn try_alloc_uninit_arc_rejects_smart_ptr_alignment() {
let arena = capped_arena();
arena.try_alloc_uninit_arc::<SmartPtrOverAlignedDrop>().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::<SmartPtrOverAlignedDrop>(1).unwrap_err();
}

#[test]
fn try_alloc_uninit_slice_box_rejects_smart_ptr_alignment() {
let arena = capped_arena();
arena.try_alloc_uninit_slice_box::<SmartPtrOverAlignedDrop>(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::<SmartPtrOverAligned>().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::<SmartPtrOverAligned>();
}

#[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::<SmartPtrOverAligned>();
}

#[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[..]);
}
10 changes: 5 additions & 5 deletions crates/multitude/src/arena/alloc_slice_arc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -419,7 +419,7 @@ impl<A: Allocator + Clone> Arena<A> {
/// [`Self::impl_alloc_slice_smart_copy`].
#[inline]
fn alloc_slice_smart_copy_raw<S: Strong, T: Copy>(&self, src: &[T]) -> Result<NonNull<u8>, AllocError> {
check_slice_arc_layout::<T>()?;
check_slice_arc_layout::<T, _>(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);
Expand Down Expand Up @@ -488,7 +488,7 @@ impl<A: Allocator + Clone> Arena<A> {
/// [`Self::impl_alloc_slice_smart_with`].
#[inline]
fn alloc_slice_smart_with_raw<S: Strong, T, F: FnMut(usize) -> T>(&self, len: usize, f: F) -> Result<NonNull<u8>, AllocError> {
check_slice_arc_layout::<T>()?;
check_slice_arc_layout::<T, _>(self)?;
if let Some((uninit, chunk_ptr)) = self.try_reserve_arc_slice::<S, T>(len) {
let chunk_ref = self.acquire_current_chunk_ref(chunk_ptr);
let slice_ptr = uninit.init_with_ptr(f);
Expand Down Expand Up @@ -644,8 +644,8 @@ impl<A: Allocator + Clone> Arena<A> {
/// 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<T>() -> Result<(), AllocError> {
if mem::align_of::<T>() >= MAX_SMART_PTR_ALIGN {
fn check_slice_arc_layout<T, A: Allocator + Clone>(arena: &Arena<A>) -> Result<(), AllocError> {
if arena.rejects_smart_ptr_align(mem::align_of::<T>()) {
return Err(AllocError::ALIGNMENT_TOO_LARGE);
}
Ok(())
Expand Down
10 changes: 5 additions & 5 deletions crates/multitude/src/arena/alloc_slice_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -229,7 +229,7 @@ impl<A: Allocator + Clone> Arena<A> {
/// Box: `Box::drop` runs `drop_in_place` on the slice eagerly. Copy fast path.
#[inline]
fn impl_alloc_slice_box_copy<T: Copy>(&self, src: &[T]) -> Result<Box<[T], A>, AllocError> {
check_slice_box_layout::<T>(src.len())?;
check_slice_box_layout::<T, _>(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);
Expand All @@ -248,7 +248,7 @@ impl<A: Allocator + Clone> Arena<A> {
#[inline]
#[cfg_attr(test, mutants::skip)] // `+= → *=` on the fill counter ⇒ infinite loop
fn impl_alloc_slice_box_with<T, F: FnMut(usize) -> T>(&self, len: usize, mut f: F) -> Result<Box<[T], A>, AllocError> {
check_slice_box_layout::<T>(len)?;
check_slice_box_layout::<T, _>(self, len)?;
// Check overflow before the refill loop.
let payload_bytes = mem::size_of::<T>().checked_mul(len).ok_or(AllocError::CAPACITY_OVERFLOW)?;
let ptr = self.reserve_slice_box::<T>(len, payload_bytes, |slot_ptr| {
Expand Down Expand Up @@ -379,8 +379,8 @@ impl<A: Allocator + Clone> Arena<A> {
/// smart-pointer header recovery. Slice length is full-width in the
/// chunk prefix.
#[inline]
fn check_slice_box_layout<T>(_len: usize) -> Result<(), AllocError> {
if mem::align_of::<T>() >= MAX_SMART_PTR_ALIGN {
fn check_slice_box_layout<T, A: Allocator + Clone>(arena: &Arena<A>, _len: usize) -> Result<(), AllocError> {
if arena.rejects_smart_ptr_align(mem::align_of::<T>()) {
return Err(AllocError::ALIGNMENT_TOO_LARGE);
}
Ok(())
Expand Down
Loading
Loading