Lifetime issues with mutable borrows to mutable slices

I have a mutable reference to a slice, I want to write a function which puts data into a slice and then advances a mutable reference by the amount of data written.

This was my first version which didn't compile.

fn extend<'a: 'p, 'p>(out_buf: &'p mut &'a mut [u8], data: impl AsRef<[u8]>) {
    let bytes = data.as_ref();
    let len = bytes.len();
    {
        let buf: &'a mut [u8] = &mut out_buf[0..len];
        buf.copy_from_slice(bytes);
    }

    *out_buf = &mut out_buf[len..];
}

At first I tried to change the lifetime bounds to 'p: 'a, but this made it impossible to call this function multiple times.

Trying a solution from here Lifetime issue when shortening mutable slice - #2 by steffahn I was able to write this function:

fn extend(out_buf: &mut &mut [u8], data: impl AsRef<[u8]>) {
    let bytes = data.as_ref();
    let len = bytes.len();

    let buf: &mut [u8] = std::mem::take(out_buf);
    buf[0..len].copy_from_slice(bytes);

    *out_buf = &mut buf[len..];
}

But I don't understand exactly why this works.

My current reasoning is that getting a mutable reference from a mutable reference forces the lifetime to be narrowed, i.e:

let pp: &'small mut &'big mut u8 = ...;
let p = &'small mut u8 = *pp; /* cannot be &'big */

/* so once I write it back ... */
*pp = p; /* requires 'small: 'big */

But I don't understand why exactly this limitation exists.

UPD: Current example that is UB Rust Playground

Why do you need a nested reference at all? Wouldn't a reborrow work for you?

I want to use a single 256 byte buffer for building and writing UDP packets.
I don't want to have to deal with the length.

I want to write code like:

extend(buffer, remote_sequence_id.to_le_bytes());
extend(buffer, local_sequence_id.to_le_bytes());
...
// and then write the buffer that I built

My function also doesn't do what I want, since this line *out_buf = &mut buf[len..]; overwrites the original buffer.

Maybe use a heapless::Vec then? It has extend() and extend_from_slice() methods.

let mut buffer = heapless::Vec::<u8, 256>::new();
buffer.extend(remote_sequence_id.to_le_bytes());
buffer.extend(local_sequence_id.to_le_bytes());
writer.write_all(&buffer);

It will work, thanks. It will also work if I create my own struct like UdpPacket and will keep the length there. But I want to understand how to make my example work also.

This is the only way I managed to make it work.
But miri says this is undefined behavior. At this point I am not sure what I am doing exactly.

Well you cannot extend a slice without knowing the current position to write to.
So you'd need to pass this information as well.

But at this point, you could also just use a Cursor, or some appropriate structure, such as heapless::Vec directly.

That's the thing, if I construct a smaller slice and put it back, then I don't need to keep track of the current position.

Afaict this would just shrink the slice for the next operation, What purpose would that have?

Yes, it will. Then after I am done. I can take the remaining length, sub it from the original length and get how many characters were written into the buffer.

Maybe return the new slice:

1 Like

Yes, this works! But now I wonder why original version doesn't.

The only way to get a &'long mut [u8] from a &'short mut &'long mut [u8] is if 'short = 'long. That's what led to the borrow checker error in your OP. But note that you don't actually want to make the lifetimes the same, as it will make the original &'long mut [u8] unusable. So extend may still compile, but main would not.


It's generally easier to explain why something doesn't pass borrow check than to explain why something else does pass borrow check, but I'll give it a shot.

In the working version, out_buf: &'a mut [u8] is reborrowed twice in extend:

    // A short reborrow that expires immediately
    out_buf[0..len].copy_from_slice(bytes);
    // A reborrow for all of `'a`
    &mut out_buf[len..]

As for the returned slice: when you have a t_ref: &'a mut T, and you reborrow *t_ref for all of 'a, you generally can think of it as moving t_ref instead -- you can't use t_ref directly again,[1] because in some sense you have given away all the access t_ref had. You don't use out_buf after you return it, so there's no borrow checker concerns in extend.

Then from the perspective of main: You have buff: &'buff mut [u8] = elements.as_mut(), then you pass a &'buff mut [u8] to extend -- that is you pass a reborrow with the entire original lifetime -- and then overwrite buff with the return value, which is also a &'buff mut [u8].

The borrow checker is okay with this as it understands that overwriting a reference doesen't read the referent data, so overwriting buff doesn't conflict with *buff being reborrowed. It's a particular special case that the borrow checker was designed to handle. So buff is unusable when you pass it in to extend but becomes reusable after the assignment -- and you don't try to use it during that period, so it's fine.

Here's a playground where you can see that trying to use buff during that period results in an error.


  1. or more precisely you can't use *t_ref through the same t_ref value again, as we'll see shortly ↩︎

3 Likes

I see, thanks!

Slightly different question, but do you know why this version is causing undefined behaviour and how to fix it?

A &mut _ implies exclusive access, and violating that is UB. It's a fundamental rule of Rust. Your start became invalid once you started using buffer directly again.

A good rule of thumb is that once you go into raw pointer land, do everything via the raw pointer until you're ready to come out of raw pointer land. So for example:

fn fill_buffer<'a>(buffer: &'a mut [u8], f: impl Fn(&mut &mut [u8])) -> &'a [u8] {
    // Get the length first
    let buf_len = buffer.len();
    // Go into raw pointer land
    let start = buffer.as_mut_ptr();
    // Recreate the buffer from that
    let p = &mut unsafe { std::slice::from_raw_parts_mut(start, buf_len) };
    // Transiently use the new `&mut _`
    let msg_len = {
        f(p);
        buf_len - p.len()
    };
    // Leave raw pointer land
    unsafe { std::slice::from_raw_parts(start, msg_len) }
}

Of course, an even better rule of thumb is "don't resort to unsafe unless you absolutely must" :slightly_smiling_face:.

Edit: Keep reading this thread to see why (the above playground isn't sound either).

2 Likes

You need to know two things:

  1. When you use slicing there's an implicit function call happening which picks a self.

  2. Exclusive references are not copyable. They can either be moved or reborrowed, like uniquely owned objects.

It's not simply &mut buf[..], it's IndexMut::index(buf, range).

When you make a function call, there's automatic adjustment of references happening. You can't index &mut &mut buf, because that's a reference to a reference, not a slice. In the call it automagically becomes &mut **buf, and the compiler has to figure out a lifetime for the newly dereferenced and reborrowed object.

When you call on the original object, with its original lifetime, this autoderef has to reborrow to give you a shorter temporary lifetime for the call. Otherwise it wouldn't uphold the exclusivity and invariance promises, because the full-length reference still exists where it was before. It's seems silly when it's &mut, but think what it would do for Vec. You can't have the same non-empty Vec in two places! You can't have two &'exclusive Vec either. There can only exist Vec and a temporary &mut Vec.

However, when you use take, you do create a new object! It returns you exclusive ownership of the non-empty slice reference, and creates a new independent empty slice reference in its place.

So now that you have exclusive ownership of the slice, &mut **buf doesn't have to reborrow, it can move it instead.

The same as if you took ownership of a Vec, leaving vec![] in its original location.

2 Likes

I see thanks! But is there a way to write this function without using unsafe? I couldn't figure out a way.

I meant just use something safe like @Schard's suggestion. Then you don't have to be concerned you may have missed something.

Case in point: Asking this question made me look at my own response again, and even though it's not always UB, it's not sound either. The closure writer can safely replace *buffer with something unrelated to the original **buffer. You'd have to ensure the length still made sense, or better yet, that it was part of the same allocation (and even, the part you expected it to be[1]).

In spirit the example is trying to "unsplit" a split borrow, and there's no purely safe way to do that, though it's possible someone has safely encapsulated the logic in a crate or such.


  1. Maybe they "returned" the start of the slice... ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.