From fa6af219d150cf69ea74b3ceee95b7500c4a300e Mon Sep 17 00:00:00 2001 From: Taiki Endo Date: Sat, 21 Feb 2026 12:41:31 +0900 Subject: [PATCH] Add helper for safer allocation --- .clippy.toml | 17 +++++ crossbeam-channel/src/alloc_helper.rs | 1 + crossbeam-channel/src/flavors/list.rs | 29 ++++----- crossbeam-channel/src/lib.rs | 5 ++ crossbeam-deque/src/alloc_helper.rs | 1 + crossbeam-deque/src/deque.rs | 30 ++++----- crossbeam-deque/src/lib.rs | 5 ++ crossbeam-epoch/src/alloc_helper.rs | 1 + crossbeam-epoch/src/atomic.rs | 20 +++--- crossbeam-epoch/src/lib.rs | 2 + crossbeam-queue/src/alloc_helper.rs | 1 + crossbeam-queue/src/lib.rs | 2 + crossbeam-queue/src/seg_queue.rs | 30 ++++----- crossbeam-skiplist/src/alloc_helper.rs | 1 + crossbeam-skiplist/src/base.rs | 13 ++-- crossbeam-skiplist/src/lib.rs | 3 + crossbeam-utils/src/alloc_helper.rs | 86 ++++++++++++++++++++++++++ 17 files changed, 190 insertions(+), 57 deletions(-) create mode 100644 .clippy.toml create mode 120000 crossbeam-channel/src/alloc_helper.rs create mode 120000 crossbeam-deque/src/alloc_helper.rs create mode 120000 crossbeam-epoch/src/alloc_helper.rs create mode 120000 crossbeam-queue/src/alloc_helper.rs create mode 120000 crossbeam-skiplist/src/alloc_helper.rs create mode 100644 crossbeam-utils/src/alloc_helper.rs diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 000000000..cd6be7c40 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,17 @@ +# Clippy configuration +# https://doc.rust-lang.org/nightly/clippy/lint_configuration.html + +allow-private-module-inception = true +avoid-breaking-exported-api = false +disallowed-names = [] +disallowed-macros = [ + { path = "std::dbg", reason = "it is okay to use during development, but please do not include it in master branch" }, +] +disallowed-methods = [ + # Use helper for safer allocation instead. + { path = "alloc::alloc::alloc", replacement = "crate::alloc_helper::Global::allocate" }, + { path = "alloc::alloc::alloc_zeroed", replacement = "crate::alloc_helper::Global::allocate_zeroed" }, + { path = "alloc::alloc::dealloc", replacement = "crate::alloc_helper::Global::deallocate" }, +] +disallowed-types = [ +] diff --git a/crossbeam-channel/src/alloc_helper.rs b/crossbeam-channel/src/alloc_helper.rs new file mode 120000 index 000000000..080779a48 --- /dev/null +++ b/crossbeam-channel/src/alloc_helper.rs @@ -0,0 +1 @@ +../../crossbeam-utils/src/alloc_helper.rs \ No newline at end of file diff --git a/crossbeam-channel/src/flavors/list.rs b/crossbeam-channel/src/flavors/list.rs index 600cc46a1..8645034d7 100644 --- a/crossbeam-channel/src/flavors/list.rs +++ b/crossbeam-channel/src/flavors/list.rs @@ -1,6 +1,6 @@ //! Unbounded channel implemented as a linked list. -use std::alloc::{alloc_zeroed, handle_alloc_error, Layout}; +use std::alloc::{handle_alloc_error, Layout}; use std::boxed::Box; use std::cell::UnsafeCell; use std::marker::PhantomData; @@ -11,6 +11,7 @@ use std::time::Instant; use crossbeam_utils::{Backoff, CachePadded}; +use crate::alloc_helper::Global; use crate::context::Context; use crate::err::{RecvTimeoutError, SendTimeoutError, TryRecvError, TrySendError}; use crate::select::{Operation, SelectHandle, Selected, Token}; @@ -83,20 +84,20 @@ impl Block { /// Creates an empty block. fn new() -> Box { - // SAFETY: layout is not zero-sized - let ptr = unsafe { alloc_zeroed(Self::LAYOUT) }; - // Handle allocation failure - if ptr.is_null() { - handle_alloc_error(Self::LAYOUT) + // unsafe { Box::new_zeroed().assume_init() } requires Rust 1.92 + match Global.allocate_zeroed(Self::LAYOUT) { + Some(ptr) => { + // SAFETY: This is safe because: + // [1] `Block::next` (AtomicPtr) may be safely zero initialized. + // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. + // [3] `Slot::msg` (UnsafeCell) may be safely zero initialized because it + // holds a MaybeUninit. + // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. + unsafe { Box::from_raw(ptr.as_ptr().cast()) } + } + // Handle allocation failure + None => handle_alloc_error(Self::LAYOUT), } - // SAFETY: This is safe because: - // [1] `Block::next` (AtomicPtr) may be safely zero initialized. - // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. - // [3] `Slot::msg` (UnsafeCell) may be safely zero initialized because it - // holds a MaybeUninit. - // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. - // TODO: unsafe { Box::new_zeroed().assume_init() } - unsafe { Box::from_raw(ptr.cast()) } } /// Waits until the next pointer is set. diff --git a/crossbeam-channel/src/lib.rs b/crossbeam-channel/src/lib.rs index 35876c160..d6a5bde99 100644 --- a/crossbeam-channel/src/lib.rs +++ b/crossbeam-channel/src/lib.rs @@ -335,9 +335,14 @@ ))] #![warn(missing_docs, unsafe_op_in_unsafe_fn)] +#[cfg(feature = "std")] +extern crate alloc; #[cfg(feature = "std")] extern crate std; +#[cfg(feature = "std")] +mod alloc_helper; + #[cfg(feature = "std")] mod channel; #[cfg(feature = "std")] diff --git a/crossbeam-deque/src/alloc_helper.rs b/crossbeam-deque/src/alloc_helper.rs new file mode 120000 index 000000000..080779a48 --- /dev/null +++ b/crossbeam-deque/src/alloc_helper.rs @@ -0,0 +1 @@ +../../crossbeam-utils/src/alloc_helper.rs \ No newline at end of file diff --git a/crossbeam-deque/src/deque.rs b/crossbeam-deque/src/deque.rs index 4727182cf..24294d8bd 100644 --- a/crossbeam-deque/src/deque.rs +++ b/crossbeam-deque/src/deque.rs @@ -1,4 +1,4 @@ -use std::alloc::{alloc_zeroed, handle_alloc_error, Layout}; +use std::alloc::{handle_alloc_error, Layout}; use std::boxed::Box; use std::cell::{Cell, UnsafeCell}; use std::cmp; @@ -12,6 +12,8 @@ use std::sync::Arc; use crossbeam_epoch::{self as epoch, Atomic, Owned}; use crossbeam_utils::{Backoff, CachePadded}; +use crate::alloc_helper::Global; + // Minimum buffer capacity. const MIN_CAP: usize = 64; // Maximum number of tasks that can be stolen in `steal_batch()` and `steal_batch_and_pop()`. @@ -1238,20 +1240,20 @@ impl Block { /// Creates an empty block. fn new() -> Box { - // SAFETY: layout is not zero-sized - let ptr = unsafe { alloc_zeroed(Self::LAYOUT) }; - // Handle allocation failure - if ptr.is_null() { - handle_alloc_error(Self::LAYOUT) + // unsafe { Box::new_zeroed().assume_init() } requires Rust 1.92 + match Global.allocate_zeroed(Self::LAYOUT) { + Some(ptr) => { + // SAFETY: This is safe because: + // [1] `Block::next` (AtomicPtr) may be safely zero initialized. + // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. + // [3] `Slot::task` (UnsafeCell) may be safely zero initialized because it + // holds a MaybeUninit. + // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. + unsafe { Box::from_raw(ptr.as_ptr().cast()) } + } + // Handle allocation failure + None => handle_alloc_error(Self::LAYOUT), } - // SAFETY: This is safe because: - // [1] `Block::next` (AtomicPtr) may be safely zero initialized. - // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. - // [3] `Slot::task` (UnsafeCell) may be safely zero initialized because it - // holds a MaybeUninit. - // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. - // TODO: unsafe { Box::new_zeroed().assume_init() } - unsafe { Box::from_raw(ptr.cast()) } } /// Waits until the next pointer is set. diff --git a/crossbeam-deque/src/lib.rs b/crossbeam-deque/src/lib.rs index cbe7cfe90..fb3f66308 100644 --- a/crossbeam-deque/src/lib.rs +++ b/crossbeam-deque/src/lib.rs @@ -92,9 +92,14 @@ ))] #![warn(missing_docs, unsafe_op_in_unsafe_fn)] +#[cfg(feature = "std")] +extern crate alloc; #[cfg(feature = "std")] extern crate std; +#[cfg(feature = "std")] +mod alloc_helper; + #[cfg(feature = "std")] mod deque; #[cfg(feature = "std")] diff --git a/crossbeam-epoch/src/alloc_helper.rs b/crossbeam-epoch/src/alloc_helper.rs new file mode 120000 index 000000000..080779a48 --- /dev/null +++ b/crossbeam-epoch/src/alloc_helper.rs @@ -0,0 +1 @@ +../../crossbeam-utils/src/alloc_helper.rs \ No newline at end of file diff --git a/crossbeam-epoch/src/atomic.rs b/crossbeam-epoch/src/atomic.rs index e034fd3d4..b293f6321 100644 --- a/crossbeam-epoch/src/atomic.rs +++ b/crossbeam-epoch/src/atomic.rs @@ -6,8 +6,9 @@ use core::fmt; use core::marker::PhantomData; use core::mem::{self, MaybeUninit}; use core::ops::{Deref, DerefMut}; -use core::ptr; +use core::ptr::{self, NonNull}; +use crate::alloc_helper::Global; use crate::guard::Guard; #[cfg(not(miri))] use crate::primitive::sync::atomic::AtomicUsize; @@ -216,15 +217,16 @@ impl Pointable for [MaybeUninit] { type Init = usize; + #[inline] unsafe fn init(len: Self::Init) -> *mut () { let layout = Array::::layout(len); - unsafe { - let ptr = alloc::alloc::alloc(layout).cast::>(); - if ptr.is_null() { - alloc::alloc::handle_alloc_error(layout); - } - ptr::addr_of_mut!((*ptr).len).write(len); - ptr.cast::<()>() + match Global.allocate(layout) { + Some(ptr) => unsafe { + let ptr = ptr.as_ptr().cast::>(); + ptr::addr_of_mut!((*ptr).len).write(len); + ptr.cast::<()>() + }, + None => alloc::alloc::handle_alloc_error(layout), } } @@ -251,7 +253,7 @@ impl Pointable for [MaybeUninit] { unsafe { let len = (*ptr.cast::>()).len; let layout = Array::::layout(len); - alloc::alloc::dealloc(ptr.cast::(), layout); + Global.deallocate(NonNull::new_unchecked(ptr.cast::()), layout); } } } diff --git a/crossbeam-epoch/src/lib.rs b/crossbeam-epoch/src/lib.rs index eaed52512..a6439c7c7 100644 --- a/crossbeam-epoch/src/lib.rs +++ b/crossbeam-epoch/src/lib.rs @@ -128,6 +128,8 @@ mod primitive { #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] extern crate alloc; +#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] +mod alloc_helper; #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] mod atomic; #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] diff --git a/crossbeam-queue/src/alloc_helper.rs b/crossbeam-queue/src/alloc_helper.rs new file mode 120000 index 000000000..080779a48 --- /dev/null +++ b/crossbeam-queue/src/alloc_helper.rs @@ -0,0 +1 @@ +../../crossbeam-utils/src/alloc_helper.rs \ No newline at end of file diff --git a/crossbeam-queue/src/lib.rs b/crossbeam-queue/src/lib.rs index 09a9f95b8..d8084d55c 100644 --- a/crossbeam-queue/src/lib.rs +++ b/crossbeam-queue/src/lib.rs @@ -20,6 +20,8 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] +mod alloc_helper; #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] mod array_queue; #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] diff --git a/crossbeam-queue/src/seg_queue.rs b/crossbeam-queue/src/seg_queue.rs index c9344fe42..7d42f9b13 100644 --- a/crossbeam-queue/src/seg_queue.rs +++ b/crossbeam-queue/src/seg_queue.rs @@ -1,4 +1,4 @@ -use alloc::alloc::{alloc_zeroed, handle_alloc_error, Layout}; +use alloc::alloc::{handle_alloc_error, Layout}; use alloc::boxed::Box; use core::cell::UnsafeCell; use core::fmt; @@ -10,6 +10,8 @@ use core::sync::atomic::{self, AtomicPtr, AtomicUsize, Ordering}; use crossbeam_utils::{Backoff, CachePadded}; +use crate::alloc_helper::Global; + // Bits indicating the state of a slot: // * If a value has been written into the slot, `WRITE` is set. // * If a value has been read from the slot, `READ` is set. @@ -69,20 +71,20 @@ impl Block { /// Creates an empty block. fn new() -> Box { - // SAFETY: layout is not zero-sized - let ptr = unsafe { alloc_zeroed(Self::LAYOUT) }; - // Handle allocation failure - if ptr.is_null() { - handle_alloc_error(Self::LAYOUT) + // unsafe { Box::new_zeroed().assume_init() } requires Rust 1.92 + match Global.allocate_zeroed(Self::LAYOUT) { + Some(ptr) => { + // SAFETY: This is safe because: + // [1] `Block::next` (AtomicPtr) may be safely zero initialized. + // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. + // [3] `Slot::value` (UnsafeCell) may be safely zero initialized because it + // holds a MaybeUninit. + // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. + unsafe { Box::from_raw(ptr.as_ptr().cast()) } + } + // Handle allocation failure + None => handle_alloc_error(Self::LAYOUT), } - // SAFETY: This is safe because: - // [1] `Block::next` (AtomicPtr) may be safely zero initialized. - // [2] `Block::slots` (Array) may be safely zero initialized because of [3, 4]. - // [3] `Slot::value` (UnsafeCell) may be safely zero initialized because it - // holds a MaybeUninit. - // [4] `Slot::state` (AtomicUsize) may be safely zero initialized. - // TODO: unsafe { Box::new_zeroed().assume_init() } - unsafe { Box::from_raw(ptr.cast()) } } /// Waits until the next pointer is set. diff --git a/crossbeam-skiplist/src/alloc_helper.rs b/crossbeam-skiplist/src/alloc_helper.rs new file mode 120000 index 000000000..080779a48 --- /dev/null +++ b/crossbeam-skiplist/src/alloc_helper.rs @@ -0,0 +1 @@ +../../crossbeam-utils/src/alloc_helper.rs \ No newline at end of file diff --git a/crossbeam-skiplist/src/base.rs b/crossbeam-skiplist/src/base.rs index 740bf280b..0034c10a7 100644 --- a/crossbeam-skiplist/src/base.rs +++ b/crossbeam-skiplist/src/base.rs @@ -1,7 +1,7 @@ //! A lock-free skip list. See [`SkipList`]. use super::equivalent::Comparable; -use alloc::alloc::{alloc, dealloc, handle_alloc_error, Layout}; +use alloc::alloc::{handle_alloc_error, Layout}; use core::cmp; use core::fmt; use core::marker::PhantomData; @@ -11,6 +11,7 @@ use core::ptr; use core::ptr::NonNull; use core::sync::atomic::{fence, AtomicUsize, Ordering}; +use crate::alloc_helper::Global; use crossbeam_epoch::{self as epoch, Atomic, Collector, Guard, Shared}; use crossbeam_utils::CachePadded; @@ -163,10 +164,10 @@ impl Node { unsafe fn alloc(height: usize, ref_count: usize) -> *mut Self { let layout = Self::get_layout(height); unsafe { - let ptr = alloc(layout).cast::(); - if ptr.is_null() { - handle_alloc_error(layout); - } + let ptr = match Global.allocate(layout) { + Some(ptr) => ptr.as_ptr().cast::(), + None => handle_alloc_error(layout), + }; ptr::addr_of_mut!((*ptr).refs_and_height) .write(AtomicUsize::new((height - 1) | (ref_count << HEIGHT_BITS))); @@ -184,7 +185,7 @@ impl Node { unsafe { let height = (*ptr).height(); let layout = Self::get_layout(height); - dealloc(ptr.cast::(), layout); + Global.deallocate(NonNull::new_unchecked(ptr.cast::()), layout); } } diff --git a/crossbeam-skiplist/src/lib.rs b/crossbeam-skiplist/src/lib.rs index caf73347d..75a566619 100644 --- a/crossbeam-skiplist/src/lib.rs +++ b/crossbeam-skiplist/src/lib.rs @@ -243,6 +243,9 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +#[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] +mod alloc_helper; + #[cfg(all(feature = "alloc", target_has_atomic = "ptr"))] pub mod base; diff --git a/crossbeam-utils/src/alloc_helper.rs b/crossbeam-utils/src/alloc_helper.rs new file mode 100644 index 000000000..7a054684c --- /dev/null +++ b/crossbeam-utils/src/alloc_helper.rs @@ -0,0 +1,86 @@ +use core::alloc::Layout; +use core::ptr::NonNull; + +// Based on unstable alloc::alloc::Global. +// +// Note: unlike alloc::alloc::Global that returns NonNull<[u8]>, +// this returns NonNull. +pub(crate) struct Global; +#[allow(clippy::unused_self)] +impl Global { + #[inline] + #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces + fn alloc_impl(&self, layout: Layout, zeroed: bool) -> Option> { + // Layout::dangling is unstable + #[inline] + #[must_use] + fn dangling(layout: Layout) -> NonNull { + // SAFETY: align is guaranteed to be non-zero + unsafe { NonNull::new_unchecked(without_provenance_mut::(layout.align())) } + } + + match layout.size() { + 0 => Some(dangling(layout)), + // SAFETY: `layout` is non-zero in size, + _size => unsafe { + #[allow(clippy::disallowed_methods)] // we are in alloc_helper + let raw_ptr = if zeroed { + alloc::alloc::alloc_zeroed(layout) + } else { + alloc::alloc::alloc(layout) + }; + NonNull::new(raw_ptr) + }, + } + } + #[allow(dead_code)] + #[inline] + #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces + pub(crate) fn allocate(self, layout: Layout) -> Option> { + self.alloc_impl(layout, false) + } + #[allow(dead_code)] + #[inline] + #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces + pub(crate) fn allocate_zeroed(self, layout: Layout) -> Option> { + self.alloc_impl(layout, true) + } + #[allow(dead_code)] + #[inline] + #[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces + pub(crate) unsafe fn deallocate(self, ptr: NonNull, layout: Layout) { + if layout.size() != 0 { + // SAFETY: + // * We have checked that `layout` is non-zero in size. + // * The caller is obligated to provide a layout that "fits", and in this case, + // "fit" always means a layout that is equal to the original, because our + // `allocate()`, `grow()`, and `shrink()` implementations never returns a larger + // allocation than requested. + // * Other conditions must be upheld by the caller, as per `Allocator::deallocate()`'s + // safety documentation. + #[allow(clippy::disallowed_methods)] // we are in alloc_helper + unsafe { + alloc::alloc::dealloc(ptr.as_ptr(), layout) + } + } + } +} + +#[inline(always)] +#[must_use] +const fn without_provenance_mut(addr: usize) -> *mut T { + // An int-to-pointer transmute currently has exactly the intended semantics: it creates a + // pointer without provenance. Note that this is *not* a stable guarantee about transmute + // semantics, it relies on sysroot crates having special status. + // SAFETY: every valid integer is also a valid pointer (as long as you don't dereference that + // pointer). + #[cfg(miri)] + unsafe { + core::mem::transmute(addr) + } + // Using transmute doesn't work with CHERI: https://github.com/kent-weak-memory/rust/blob/0c0ca909de877f889629057e1ddf139527446d75/library/core/src/ptr/mod.rs#L607 + #[cfg(not(miri))] + { + addr as *mut T + } +}