Skip to content

#95295 causes unsoundness in multiple existing crates #101899

Closed
@LegionMammal978

Description

@LegionMammal978

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 in core:
    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()

Metadata

Metadata

Assignees

No one assigned

    Labels

    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.regression-from-stable-to-betaPerformance or correctness regression from stable to beta.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions