Strange behavior: Writing to a pointer or not doesn't affect the outcome

I have a very strange behavior in my hobby project, where it doesn't matter if I write to a pointer or not. I will try to explain my context below, I fear this is going to be a lengthy description.

What am I building

I have created (or trying to create) an async executor, that doesn't use the standard library no the alloc create, i.e. I don't use dynamic allocation. Additionally I am not using macros to generate code during compile time to store the futures that are spawned unto the executor. This means I need a different way to store these futures, which I will describe below.

Why?

Fun and learning. I learned a lot (or at least came across a lot of interesting topics) and I had most of the time fun doing so. Currently I have no intentions on doing something more with this project.

Storing futures

In order for the executor to poll the futures, it needs to have a pinned pointer to it. The way I wanted to resolve this, is by handling a & mut [u8] slice to the executor and copying the futures that are spawned unto the executor into this slice. More or less, I allocate the space for the future, get a pointer to it the allocated space and write the future to the pointer.

You can find the allocator code here: slot_allocator.

Since Future is only a trait, I need to be able to handle trait objects (or only being able to store one particular type that implements Future). To do this I wrote some kind of helper, that is similar to Box but that works with the aforementioned slot allocator: slot_box. SlotBox does also provide a method to pin the future, which comes in handy for the executor, as you might imagine.

The strange behavior

The strange thing is: If I write to the allocated pointer or not, does not matter:

My tests as well as my simple usage example behave exactly the same, no matter if I write to the pointer or not:

The question

Has anyone any idea, what is happening? From my perspective the tests should panic, since I take a Pin on an array that is zeroed and then poll it. I am confused, to say the least. I hope someone of you knows what is wrong or can nudge my in a direction where to investigate further.

It may be this:

pub unsafe fn as_mut<'a>(&mut self) -> &'a mut T

Returns a unique reference to the value. If the value may be uninitialized, as_uninit_mut must be used instead.

You could try as_ptr instead.

1 Like

I think it's just that all zero bytes happens to be the representation of a future with no arguments in the not-started state, as this demonstrates.

(As you might imagine, this isn't guaranteed)

2 Likes

Thank you for your answer. I get what you mean: An unpolled simple future is stored as a number of zeros. What I don't understand is, that the futures that I use still return a value:

let fut = async { 5 };

I would assume that this information is also stored in the future. Because, I use the pin to the memory that I allocated. However when I don't write to the future I still get the correct value back.

Can you help me out here with my misunderstanding?

Thank you for your answer. I will try this out tonight. Still don't understand why not writing to the reference does result in the same outcome.

Not necessarily - it can be statically known.

2 Likes

Not sure if this is the cause of the bug, but here you're dropping an unitialized T since assignment drops the value that is overwritten, but there's no value at that location in your case. Use ptr::write instead.

Edit: and SlotBox::pin is also unsound because it violates the Drop guarantee (currently because it doesn't drop the T, but even if it did it could be leaked and the drop guarantee would still be violated when the SlotAllocator is dropped and the pinned memory array goes out of scope; AFAIK there's no way to provide this pinning as a safe API)

1 Like

Sure, it basically is a constant that us returned. But I still poll the pinned future, how does the program now which static to return? Is this some compiler optimization and my examples are just to simple?

Thanks for the answer.

As far as I understood, you propose that:

  • that I get a pointer to my unitialized T, with NonNull::as_ptr
  • Use ptr::write to write the given value to this pointer

Regarding the unsound SlotBox::pin:

  • So from your perspective: SlotBox::pin should be unsafe
  • When I drop the SlotBox, I don't drop the value T. This is currently my intention since I want to use the Pin afterwards, to poll the pinned future.
  • Yes, the value would be kept into the handed memory as long as it is not cleared. This means the future there is leaked unless I have some clean-up phase. E.g. overriding the memory area when the future is ready, and removing the Pin that is stored in an additional array. Or am I wrong?

Since you used dyn Future: it's a function pointer in the vtable for <{async fn test_future} as Future>::poll, and that vtable is pointed to by the &mut dyn Future, completely separate from the data pointer (so the correct initialization of the data only matters for what that function decides to do based on the data). If you hadn't used dyn, it would be statically dispatched, i.e. the compiler knows what function the poll() should be and inserts the right call in the machine code.

All this is just like any other usage of any other trait.

1 Like

Yes, that would be the proper way to write to an uninitialized memory location

Yes, or at least I don't see a way to make it safe.

I didn't initially realize the method consumed self and returned a pinned reference with the 'a lifetime. It would still make sense to drop the T when the SlotBox is dropped (otherwise anything put in a SlotBox would always be leaked!) and you could leak the SlotBox only when pin is called... but this still doesn't solve the issue of the pin guarantee being broken.

I'm not sure what clean up you're referring to here. What I meant is that before the memory array used to create the SlotAllocator is dropped, you need to drop the T that was given to SlotBox and then pinned. And IMO this becomes impossible to do safely the moment you create a Pin<&'a mut T>

Every async block has it's own anonymous type, so this value is "stored" in the type.

This is the same as with closures, actually. Consider the following (contrived) example:

#[inline(never)]
fn foo(f: &dyn Fn() -> u64) -> u64 {
    f()
}

pub fn bar() -> u64 {
    foo(&|| 22) + foo(&|| 20)
}

It compiles to the following assembly (playground):

playground::foo:
	mov	rax, rdi
	lea	rdi, [rip + .L__unnamed_1]
	jmp	rax

playground::bar:
	push	rbx
	lea	rdi, [rip + playground::bar::{{closure}}]
	call	playground::foo
	mov	rbx, rax
	lea	rdi, [rip + playground::bar::{{closure}}]
	call	playground::foo
	add	rax, rbx
	pop	rbx
	ret

playground::bar::{{closure}}:
	mov	eax, 22
	ret

playground::bar::{{closure}}:
	mov	eax, 20
	ret

.L__unnamed_1:

What we see here is that each closure got its own code block, and each call to foo had the corresponding code block provided to it explicitly.

Thanks for the reply.

So as far as I understand it, although I am going through my SlotBox and my SlotAllocator the Rust compiler knows which poll methods I actually want to call during compilation. And since my futures don't have any real state, the data written into the [u8] slice is irrelevant, since the futures don't require the state anyway (since they are just returning a value, or in my simple example, checking the time against a value etc.).

Thank you for your reply.

I guess the Rust compiler understands which Future I want to poll and calls the poll method for this future, more or less ignoring its state which I wrote (or not wrote dependent on the branch) to the [u8] slice. It simply doesn't matter.

No, every auto-generated async {...} future has state. At a minimum, they will always panic if you poll them after they return Ready from a previous poll, which requires two states.

(On the other hand, closure types can be zero-sized when the closure doesn't capture anything.)

5 Likes

Thanks again. I have some further questions:

SlotBox is like a helper method, so I intentionally leak the T behind SlotBox. What I actually want is this:

I have a spawn method in my Exector:
Executor::spawn

In this spawn method I get a impl Future<Output=()> + 'a which I want to store in a & mut [u8] slice. At least this is my understanding, that a Future needs some memory location and a Pin to this location. I would omit the whole SlotBox but then I still need a Pin to the location in the slice of type Pin<& mut dyn Future<Output=()> + 'a>. Therefore I create the SlotBox (see src/lib.rs · pointer_write · huntrss / aeons · GitLab) which should write the future to the slice (which it doesn't, due to reasons you and the others have pointed out). I then immediately consume the SlotBox to get the Pin<& mut dyn Future> I actually want (see src/lib.rs · pointer_write · huntrss / aeons · GitLab). I would use a function that would write the Future to my slice and creates me a Pin but I couldn't get it work, since I need to somehow cast an impl Future to a dyn Future which I did not manage. But this also means, that the Future is not dropped, and therefore I guess you're correct, that I break this guarantee.

My idea is to zero the memory I used for the future (in the spawn method) to zero these bytes, when the Future is Poll::ready. For example here: src/lib.rs · pointer_write · huntrss / aeons · GitLab

Thanks again.

And this state is written to the Pointer I provide via the Pin, independent of the fact that I write to this Pointer the value I provided in SlotBox::new. So calling poll will write to this state if necessary, but the initial state seems to be currently just zeroed-out.

I think I now understand better:

Drop::guarantee

Notice that this guarantee does not mean that memory does not leak! It is still completely okay to not ever call drop on a pinned element (e.g., you can still call mem::forget on a Pin<Box<T>>). In the example of the doubly-linked list, that element would just stay in the list. However you must not free or reuse the storage without calling drop.

It would be "ok" to leak the memory, but I actually want to repurpose the memory, but I don't call drop on the Future that was stored there when it became ready.

You have a Rust-only crate with lots of unsafe code, so you should definitely test it with Miri.

$> rustup toolchain add nightly
$> rustup component add miri --toolchain nightly
$> cargo +nightly miri test

It gives a following error (edited):

test slot_box::tests::storing_pinned_futures_in_an_array ... error: Undefined Behavior: trying to retag from <269056> for SharedReadWrite permission at alloc70558[0x0], but that tag does not exist in the borrow stack for this location
   --> src/slot_box.rs:128:34
    |
128 |         pinned_futures[0] = Some(future);
    |                                  ^^^^^^
    |                                  |
    |   trying to retag from <269056> for SharedReadWrite permission at alloc70558[0x0], but that tag does not exist in the borrow stack for this location

help: <269056> was created by a SharedReadWrite retag at offsets [0x0..0x2]
   --> src/slot_box.rs:114:58
    |
114 | let future: Pin<&mut dyn Future<Output = u32>> = slot_box.pin();
    |                                                          ^^^^^^^^^^^^^^
help: <269056> was later invalidated at offsets [0x0..0x200] by a Unique retag
   --> src/slot_allocator.rs:48:42
    |
48  | let pointer = NonNull::from(&mut self.memory[start_address..end_address]);
    |                                          ^^^^^^^^^^^
    = note: inside `slot_box::tests::storing_pinned_futures_in_an_array` at src/slot_box.rs:128:34: 128:40

The text is pretty opaque if you see it for the first time and aren't familiar with the Stacked Borrows memory model, but the important part is visible: you create several simultaneous &mut references to the same memory location. Specifically, they are

let future: Pin<&mut dyn Future<Output = u32>> = slot_box.pin();
             // ^^^^
let pointer = NonNull::from(&mut self.memory[start_address..end_address]);
                         // ^^^^

That's a big No-No in Rust. A &mut reference is commonly called Unique Reference, because its creation asserts to the compiler that there are no other live pointers to the same memory region simultaneously existing. Violating that rule is instant Undefined Behaviour, turning your program into a program-shaped garbage pile. The rule of thumb is that you should work either with safe references or with raw pointers exclusively, and almost never convert between them. Any such conversion requires extra scrutiny and proper safety proofs, because they are very likely to run afoul of Rust's memory model. Creating &mut from some passed-in raw pointers is particularly dangerous, you never know who's also holding a copy of that pointer, and simply doing &mut *ptr locally can cause UB.

In fact, your code invokes this kind of UB unconditionally in any nontrivial scenario, because SlotAllocator is defined as

pub(crate) struct SlotAllocator<'a> {
    memory: Pin<&'a mut [u8]>,
    max_memory_size_slot: usize,
    max_slots: usize,
}

memory has type Pin<&mut [u8]>, which carries (mostly) the same aliasing restrictions as &mut [u8] --- no other pointers inside of this buffer may exist simultaneously with memory, which means you can never use it to allocate anything. More precisely, the uniqueness is asserted whenever memory is accessed (read/write), so while you can create new pointers inside the region, they will be all invalidated as soon as you do any operation on memory, including subslicing it to allocate a new object.

If you want to write an allocator, you really should handle the allocated chunk strictly as a raw pointer (*mut u8), without creating safe references to it at any point, until you have returned the allocated object.


The type of the allocation buffer is also wrong. &mut [u8] means that the entire buffer at all times consists of initialized memory, but that's not true. You blindly copy value: T into a fresh allocation. The type of T may, and likely will have padding between its fields. The precise nature of padding in the Rust language is a bit complicated, but to a first approximation you should treat it as uninitialized memory. This means that you copy uninitialized memory into the memory buffer, making the target of the write also uninitialized. Boom, UB. memory: Pin<&mut [u8]> asserts to the compiler that it can always safely read from that buffer at its leisure, and you have broken that promise.


In fact, having a type Pin<&mut [u8]> means that you deeply misunderstand the meaning of Pin, the guarantees it provides and the requirements it imposes. [u8] is Unpin, which means that pinning &mut [u8] has absolutely no effect (and can also be done safely, using Pin::new). So the type of memory is complicated for no good reason, and just serves to introduce new potential footguns.


Also, you use a lot of static mut items in your examples. The basic rules of using static mut in Rust is don't. The core issue is the aliasing restrictions on &mut which I have discussed above, which mean that it is extremely difficult to use static mut items without running foul of UB. It's possible, and there are certain use cases (like programming embedded devices) which require it, but it should only be used as an absolute last resort, and with extreme care.

5 Likes

Thanks for your answer really appreciated.

I hope you still have patience with me regarding a few questions. I will write later back, because I currently have no time at the moment.