Empty &mut [T] inside another &mut [T]

Is it undefined behavior to create a mutable reference to an empty slice which lies within another slice with a live mutable reference?

Background: I'm writing an arena allocator for a specialized use case, and that allocator supports dynamically extending an allocation if that allocation is the last one in its memory chunk. This can lead to the following sequence of operations:

  1. alloc(8) which reserves 8 bytes starting at address, say, 0x1000, and returns a &mut [u8] of length 8.
  2. alloc(0) which reserves 0 bytes and returns a &mut [u8] of length 0.
  3. extend(0x1000, 4), which extends the allocation at 0x1000 by 4 bytes and returns a &mut [u8] of length 12, still starting at address 0x1000 to avoid the memcpy.

Currently, the allocator uses NonNull::dangling() for creating the zero-sized allocation in step 2, but I would like to remove that special case and just return a slice of length 0 starting at address 0x1008. That would mean that there are two live, mutables references to slices, one from 0x1000 to 0x100c, the other from 0x1008 to 0x1008. Would that be undefined behavior? I don't think so, because there is no memory that is accessible from different mutable references, but I'm not sure what the exact requirements are.

1 Like

This seems fine to me. As I understand them, the guarantees for immutable and mutable references are respectively:

  1. Immutability guarantee. An immutable reference guarantees that between any two uses of the same immutable reference, the value has not been modified.
  2. Uniqueness guarantee. A mutable reference guarantees that between any two uses of the same mutable reference, no other pointer or reference has accessed the value. It also guarantees that if the mutable reference was used between any two uses of any other pointer/reference to the same value, then the mutable reference was created from that other pointer/reference (possibly recursively).

Note that copying, offseting or casting of a raw pointer returns a raw pointer that is considered "the same" raw pointer in the eyes of the created-from rule, so all such copies would be parents of mutable references created from any one of the copies.

Note also that any raw pointers derived from an immutable reference are also subject to the immutability guarantee.

2 Likes

as far as I understand mutable references to zsts (like () or [T; 0]) cannot alias
so:

let a = &mut *(0x1008 as *mut ());
let b = &mut *(0x1008 as *mut ());

should be fine

1 Like

Honestly, the best way to find out is to run your code under miri with stacked borrows turned on.

That doesn't necessarily work though. Miri can't detect all such violations, it produces some false negatives.

as far as I understand mutable references to zsts (like () or [T; 0]) cannot alias

ZSTs are a good keyword. I found this discussion, where the consensus is that values of ZSTs don't alias with any other values.

miri

The following code is accepted by Miri:

use std::alloc;
use std::slice::from_raw_parts_mut;
fn main() {
    unsafe {
        let layout = alloc::Layout::from_size_align(4, 1).unwrap();
        let chunk_ptr = alloc::alloc(layout);
        chunk_ptr.copy_from_nonoverlapping([10, 11, 12, 13].as_ptr(), 4);
        let slice_1: &mut [u8] = from_raw_parts_mut(chunk_ptr, 4);
        let slice_2: &mut [u8] = from_raw_parts_mut(chunk_ptr.add(2), 0);
        // let slice_3: &mut [u8] = from_raw_parts_mut(chunk_ptr.add(2), 1);
        println!("slice_1: {:?}", slice_1);
        println!("slice_2: {:?}", slice_2);
        alloc::dealloc(chunk_ptr, layout);
    }
}

but UB is flagged when the let slice_3 = ... line is uncommented. So Miri detects overlapping &mut slices, but is fine with empty &mut slices. Of course, like H2CO3 notes, this might be a false negative.

1 Like