Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to call `.as_ref()` or `.as_mut()` on `NonNull<[u8]>` from `Allocator::allocate()` before initialization? (Rust `allocator_api`)

Clearly, a lot of thought has gone into the design of Allocator::allocate in Rust's unstable allocator_api. However, I'm struggling with a subtle question:

Is it safe to immediately call .as_ref() or .as_mut() on the returned NonNull<[u8]>, even before initializing the memory? Or can that cause Undefined Behavior (UB)?

🧠 My thought process

  • The function returns a NonNull<[u8]>, not NonNull<[MaybeUninit<u8>]>, which implies it's meant to be ergonomic to use.
  • Does Rust even know any uninitialized state for u8 in general? I don't think so, but if yes, I wonder if the reference creation itself is UB.
  • Reading uninitialized memory is obviously UB — but is creating a reference to it also UB?

Miri does not complain about the code below, so that suggests it’s fine.

🖊️ Example

#![feature(allocator_api)]

use std::alloc::{Allocator, GlobalAlloc, Layout, System};

fn main() {
    let layout = Layout::from_size_align(4096, 4096).unwrap();
    // Memory coming from external world
    let mut buffer_ptr: NonNull<[u8]> = System.allocate(layout).unwrap();
    // Is creating the reference UB or not? Miri doesn't complain.
    let buffer = unsafe { buffer_ptr.as_mut() };
    buffer[1] = 42;
    let _x = buffer[1]; // ✅ OK, initialized
    let _y = buffer[2]; // Clearly UNDEFINED BEHAVIOR, uninitialized memory
    unsafe {
        System.dealloc(buffer_ptr.as_ptr().cast(), layout);
    }
}

⚖️ Counterpoint from another developer

A colleague argues that this is UB unless the memory is first initialized (e.g., via ptr::write, MaybeUninit, or a type that allows uninitialized state). Their claim is that even forming the reference (as_mut()) is UB unless the memory content is fully initialized, because u8 is not allowed to be uninitialized.

🤔 The core question

Does creating a &mut [u8] from a NonNull<[u8]> pointing to uninitialized memory violate Rust's aliasing or initialization rules—even if no uninitialized memory is read?

If this is guaranteed safe, then why doesn't the API use MaybeUninit<u8> instead of u8?

In case my assumption is true, then also initializing the memory using slice indices (buffer[1] = 42;) is perfectly fine.


This has relevance for some design decisions in the uefi-rs project, which makes use of custom allocators and needs to reason carefully about pointer aliasing and initialization.

like image 506
phip1611 Avatar asked Nov 18 '25 07:11

phip1611


1 Answers

This is undefined behaviour according to the Rust Reference.

The relevant restrictions here are:

  • "[to be valid, an] integer (i*/u*), floating point value (f*), or raw pointer must be initialized, i.e., must not be obtained from uninitialized memory.", and
  • "A reference or Box<T> must be aligned and non-null, it cannot be dangling, and it must point to a valid value […]. Note that the last point (about pointing to a valid value) remains a subject of some debate."

So it looks like under the present undefined behaviour rules, the compiler is allowed to assume that you won't do that (and, e.g., optimise any code which attempts to form a reference to uninitialized u8s into a no-op). But it also seems as though it hasn't yet been decided whether that will eventually be permitted or not in a future version of Rust, and the reason that it isn't allowed at the moment is to allow flexibility for adapting the rules for references to uninitialized data in the future.

like image 159
ais523 Avatar answered Nov 20 '25 01:11

ais523



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!