Description
I was recently looking through the draft release notes when I noticed #95295. While it makes sense for Layout::from_size_align()
to restrict allocations to isize::MAX
bytes, this restriction was also added to Layout::from_size_align_unchecked()
, which is a public and widely used API. Some crates were sound under the previous overflow property, usually panicking or returning an error after checking the Layout
against isize::MAX
. However, these have become unsound under the new overflow property, since just constructing the overlarge Layout
is now UB. Also, some crates created overlarge layouts for the sole purpose of feeding them into handle_alloc_error()
. To list the instances I've found:
- The provided
GlobalAlloc::realloc()
impl incore
:use std::alloc::{GlobalAlloc, Layout, System}; struct Alloc; // SAFETY: Wraps `System`'s methods. unsafe impl GlobalAlloc for Alloc { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { System.alloc(layout) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { System.dealloc(ptr, layout) } } let alloc = Alloc; // SAFETY: The layout has non-zero size. let ptr = unsafe { alloc.alloc(Layout::new::<u8>()) }; assert!(!ptr.is_null()); // SAFETY: // - `ptr` is currently allocated from `alloc`. // - The layout is the same layout used to allocate `ptr`. // - The new size is greater than zero. // - The new size, rounded up to the alignment, is less than `usize::MAX`. unsafe { alloc.realloc(ptr, Layout::new::<u8>(), isize::MAX as usize + 1) }; // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at <Alloc as core::alloc::GlobalAlloc>::realloc()
semver
v1.0.14:// --target i686-unknown-linux-gnu use semver::BuildMetadata; let s = String::from_utf8(vec![b'0'; isize::MAX as usize - 4]).unwrap(); s.parse::<BuildMetadata>().unwrap(); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at semver::identifier::Identifier::new_unchecked()
hashbrown
v0.12.3:// features = ["raw"] use hashbrown::raw::RawTable; assert!(cfg!(target_feature = "sse2")); RawTable::<u8>::with_capacity(usize::MAX / 64 * 7 + 8); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 17, 16) // at hashbrown::raw::TableLayout::calculate_layout_for()
rusqlite
v0.28.0 (admittedly contrived):// --target i686-unknown-linux-gnu // features = ["bundled", "vtab"] use rusqlite::{ ffi, vtab::{ self, sqlite3_vtab, sqlite3_vtab_cursor, Context, IndexInfo, VTab, VTabConnection, VTabCursor, Values, }, Connection, }; use std::os::raw::c_int; #[repr(C)] struct DummyTab { base: sqlite3_vtab } // SAFETY: `DummyTab` is `repr(C)` and starts with a `sqlite3_vtab`. unsafe impl<'vtab> VTab<'vtab> for DummyTab { type Aux = (); type Cursor = DummyCursor; fn connect( _: &mut VTabConnection, _: Option<&Self::Aux>, _: &[&[u8]], ) -> rusqlite::Result<(String, Self)> { let s = String::from_utf8(vec![b'\x01'; isize::MAX as usize]).unwrap(); Err(rusqlite::Error::SqliteFailure(ffi::Error::new(0), Some(s))) } fn best_index(&self, _: &mut IndexInfo) -> rusqlite::Result<()> { unimplemented!() } fn open(&'vtab mut self) -> rusqlite::Result<Self::Cursor> { unimplemented!() } } #[repr(C)] struct DummyCursor { base: sqlite3_vtab_cursor } // SAFETY: `DummyCursor` is `repr(C)` and starts with a `sqlite3_vtab_cursor`. unsafe impl VTabCursor for DummyCursor { fn filter(&mut self, _: c_int, _: Option<&str>, _: &Values<'_>) -> rusqlite::Result<()> { unimplemented!() } fn next(&mut self) -> rusqlite::Result<()> { unimplemented!() } fn eof(&self) -> bool { unimplemented!() } fn column(&self, _: &mut Context, _: c_int) -> rusqlite::Result<()> { unimplemented!() } fn rowid(&self) -> rusqlite::Result<i64> { unimplemented!() } } let conn = Connection::open_in_memory().unwrap(); let module = vtab::eponymous_only_module::<DummyTab>(); conn.create_module("dummy", module, None).unwrap(); conn.execute("SELECT * FROM dummy", ()).unwrap(); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at rusqlite::util::sqlite_string::SqliteMallocString::from_str()
allocator_api
v0.6.0:use allocator_api::RawVec; let mut raw_vec: RawVec<u8> = RawVec::new(); raw_vec.reserve(0, isize::MAX as usize + 1); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at <core::alloc::Layout as allocator_api::libcore::alloc::LayoutExt>::repeat() // at <core::alloc::Layout as allocator_api::libcore::alloc::LayoutExt>::array::<u8>() // at allocator_api::liballoc::raw_vec::RawVec::<u8, allocator_api::global::Global>::reserve_internal()
pyembed
v0.22.0:// pyo3 = "0.16.5" use pyembed::{MainPythonInterpreter, MemoryAllocatorBackend, OxidizedPythonInterpreterConfig}; use pyo3::types::PyByteArray; let interpreter = MainPythonInterpreter::new(OxidizedPythonInterpreterConfig { allocator_backend: MemoryAllocatorBackend::Rust, set_missing_path_configuration: false, ..Default::default() }) .unwrap(); interpreter.with_gil(|py| { let array = PyByteArray::new(py, b""); array.resize(isize::MAX as usize - 15).unwrap(); }); // calls Layout::from_size_align_unchecked(isize::MAX as usize - 14, 16) // at pyembed::pyalloc::rust_malloc()
cap
v0.1.1:use cap::Cap; use std::alloc::{GlobalAlloc, Layout, System}; let alloc = Cap::new(System, usize::MAX); // SAFETY: The layout has non-zero size. let ptr = unsafe { alloc.alloc(Layout::new::<u8>()) }; assert!(!ptr.is_null()); // SAFETY: // - `ptr` is currently allocated from `alloc`. // - The layout is the same layout used to allocate `ptr`. // - The new size is greater than zero. // - The new size, rounded up to the alignment, is less than `usize::MAX`. unsafe { alloc.realloc(ptr, Layout::new::<u8>(), isize::MAX as usize + 1) }; // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at <cap::Cap<std::alloc::System> as core::alloc::GlobalAlloc>::realloc()
scoped-arena
v0.4.1:use scoped_arena::Scope; Scope::new().to_scope_many::<u8>(0, 0); // calls Layout::from_size_align_unchecked(usize::MAX - 1, 1) // at scoped_arena::Scope::<'_, scoped_arena::allocator_api::Global>::to_scope_many::<u8>()
Also, many more crates were sound under the condition that alloc::alloc()
always fails on allocations larger than isize::MAX
bytes, but likely unsound if it were to successfully return an allocated pointer. Before #95295, they would either panic, return an error, or call handle_alloc_error()
from alloc()
failing to satisfy the overlarge request. Many of these crates have now become unconditionally unsound after the change.
Now-unsound crates that depended on overlarge alloc() failing
bumpalo
v3.11.0:// debug-assertions = false use bumpalo::Bump; Bump::try_with_capacity(isize::MAX as usize + 1).unwrap_err(); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at bumpalo::layout_from_size_align() // at bumpalo::Bump::try_with_capacity()
async-task
v4.3.0:// --target i686-unknown-linux-gnu use std::{future, mem, task::Waker}; const SIZE: usize = isize::MAX as usize - mem::size_of::<Option<Waker>>() - 10; let _ = async_task::spawn(future::pending::<[u8; SIZE]>(), |_| {}); // calls Layout::from_size_align_unchecked(isize::MAX as usize - 2, 4) // at async_task::utils::Layout::into_std() // at async_task::raw::RawTask::<core::future::Pending<[u8; {_}]>, [u8; {_}], {closure}>::eval_task_layout()
zerocopy
v0.6.1:// features = ["alloc"] use zerocopy::FromBytes; u8::new_box_slice_zeroed(isize::MAX as usize + 1); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at <u8 as zerocopy::FromBytes>::new_box_slice_zeroed()
memsec
v0.6.2:// --target x86_64-unknown-linux-gnu // libc = "0.2.64" use libc::_SC_PAGESIZE; // SAFETY: `_SC_PAGESIZE` is a valid `sysconf` argument. let page_size = unsafe { libc::sysconf(_SC_PAGESIZE) as usize }; assert!(page_size != usize::MAX); let size = isize::MAX as usize - page_size * 5 - 13; // SAFETY: No preconditions. unsafe { memsec::malloc_sized(size) }; // calls Layout::from_size_align_unchecked(isize::MAX as usize - page_size + 1, page_size) // at memsec::alloc::raw_alloc::alloc_aligned()
bevy_ecs
v0.8.1:// --target i686-unknown-linux-gnu use bevy_ecs::component::{Component, Components}; #[derive(Component)] #[component(storage = "SparseSet")] struct Data([u8; usize::MAX / 128 + 1]); Components::default().init_component::<Data>(&mut Default::default()); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at bevy_ecs::storage::blob_vec::repeat_layout() // at bevy_ecs::storage::blob_vec::array_layout() // at bevy_ecs::storage::blob_vec::BlobVec::grow_exact()
lasso
v0.6.0:// debug-assertions = false use lasso::{Capacity, Rodeo}; let bytes = (isize::MAX as usize + 1).try_into().unwrap(); let _: Rodeo = Rodeo::with_capacity(Capacity::new(0, bytes)); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at lasso::arena::Bucket::with_capacity()
thin-dst
v1.1.0:use thin_dst::ThinBox; struct DummyIter; impl Iterator for DummyIter { type Item = u8; fn next(&mut self) -> Option<Self::Item> { unimplemented!() } } impl ExactSizeIterator for DummyIter { fn len(&self) -> usize { isize::MAX as usize + 1 } } ThinBox::new((), DummyIter); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at thin_dst::polyfill::alloc_layout_extra::repeat_layout() // at thin_dst::polyfill::alloc_layout_extra::layout_array::<u8>() // at thin_dst::ThinBox::<(), u8>::layout()
lightproc
v0.3.5:// --target i686-unknown-linux-gnu use lightproc::lightproc::LightProc; use std::{ future::Future, pin::Pin, task::{Context, Poll}, }; #[repr(align(4))] struct Dummy; impl Future for Dummy { type Output = [u8; isize::MAX as usize - 2]; fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> { unimplemented!() } } LightProc::build(Dummy, |_| {}, Default::default()); // calls Layout::from_size_align_unchecked(isize::MAX as usize - 2, 4) // at lightproc::raw_proc::RawProc::<Dummy, [u8; {_}], {closure}>::proc_layout()
thin-vec
v0.2.8:// --target x86_64-unknown-linux-gnu use thin_vec::ThinVec; ThinVec::<u8>::with_capacity(isize::MAX as usize - 21); // calls Layout::from_size_align_unchecked(isize::MAX as usize - 6, 8) // at thin_vec::layout::<u8>() // at thin_vec::header_with_capacity::<u8>()
bsn1
v0.4.0:// --target i686-unknown-linux-gnu use bsn1::{Der, IdRef}; struct Iter<'a>(Option<&'a [u8]>); impl Clone for Iter<'_> { fn clone(&self) -> Self { Self(Some(&[0; 7])) } } impl<'a> Iterator for Iter<'a> { type Item = &'a [u8]; fn next(&mut self) -> Option<Self::Item> { self.0.take() } } let vec = vec![0; isize::MAX as usize - 1]; Der::from_id_iterator(IdRef::eoc(), Iter(Some(&vec))); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at bsn1::buffer::Buffer::reserve()
seckey
v0.11.2:// default-features = false // features = ["use_std"] use seckey::SecBytes; SecBytes::new(isize::MAX as usize + 1); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at seckey::bytes::alloc::malloc_sized()
slice-dst
v1.5.1:use slice_dst::SliceDst; <[u8]>::layout_for(isize::MAX as usize + 1); // calls Layout::from_size_align_unchecked(isize::MAX as usize + 1, 1) // at slice_dst::layout_polyfill::repeat_layout() // at slice_dst::layout_polyfill::layout_array::<u8>() // at <[u8] as slice_dst::SliceDst>::layout_for()
stable-vec
v0.4.0:use stable_vec::ExternStableVec; ExternStableVec::<u16>::with_capacity(usize::MAX / 4 + 1); // calls Layout::from_size_align_unchecked(isize::MAX as usize, 2) // at <stable_vec::core::bitvec::BitVecCore<u16> as stable_vec::core::Core<u16>>::realloc()