Can a future used only by `async fn` assume it won't be polled again after becoming ready?

Are there any guarantees about what an async fn will do if its future is polled again after it completes? In particular, given the following situation…

pub async fn foo(foo: Foo) {
    // We await the Foo, then never use it again.
    foo.await;
}

struct Foo;

impl Future for Foo {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        todo!();
    }
}

…I'm wondering whether Foo::poll can assume that it will never be called again after returning Poll::Ready. I know that assumption is not sound in general, but I can imagine that in the case where you know all callers are .await expressions in async fns that own the future it might be okay, assuming that the async fns already have their own "panic if polled again after completion" logic internally.

1 Like

this is only true, if you are in control of both the caller and callee.

but this is not specific to Future polling, it's just you have the knowledge of how a piece of code will be used. if your type can be used by third party code, then nothing can be said for sure.

as far as I know, the synthetic Future of the async fn sugar would indeed panic if polled again after Ready, but it is only for the synthetic type itself, since it is desugared like a state machine.

but it guarantees nothing about the behavior of "inner" futures, which is completely determined how the async fn is using the type.

for "normal" code, you only .await the future once, but note this blanket implementation in the standard library:

impl<F> Future for &mut F where F: Future {
    //...
}

this can be abused to make it possible to .await a future multiple times, and there's no unsafe needed. example:

2 Likes

The current implementation of async fn will never invoke poll of a sub-future after Ready.

But in principle I could see a possibility that if you poll an async fn future after Ready, it could be okay for that future to invoke poll on the last .await statement in its body again.

3 Likes

you should indeed assume that it won't be polled again, which is why you should panic! if it is.

in the current impl .await will not poll again after receiving ready, and all async block will panic! if polled after completion.

if you are not doing anything unsafe, then anything is a reasonable to do if being polled after completion though panic! is prob best.

1 Like

If you assume it won't be polled again then there's no reason to put a panic!() there, because it will never be hit.

If instead you want it to never be polled again, but it might be, then you want to put a panic or something similar.

i disagree. panic is for bugs, and as such for broken assumptions. if you assume a code path wil not be reached but cannot prove that it won't. then you should put a panic there because if it is reached then you will know an error has happened.

ultimately this is semantics about what we mean by assume. but when i read "assume", i expect "not certain", or at least "unable to prove", which is where panic! fits

I would expect no.

In particular, your example is the same as

pub async fn foo(foo: Foo) {
    return foo.await;
}

and I suspect the compiler would like the freedom to say "hey, I see that the only .await is returned", and thus just have poll forward on to the inner future -- that avoids adding an unnecessary wrapper at every level.

(Kinda like how iter::Map just always calls next on the underlying iterator without tracking itself whether that underlying iterator is exhausted.)

3 Likes

Thanks all. To be clear, I am talking about a situation where I control caller and callee, and relying on this for soundness. If such a guarantee existed from async fn, then it would be a waste of CPU cycles to check for it downstream again in a branch that can never be taken.

It sounds like the answer is approximately "this is how async fn works today, but it's not guaranteed". Is that correct?

If so, it seems a little unfortunate to have to double check this everywhere. While it's true that not guaranteeing this makes a potential optimization possible, it makes the same optimization on the callee end impossible today, despite the fact that it could otherwise totally work. It seems like one direction or the other could be guaranteed.

You can rely on it except where it would cause Undefined Behavior. Your implementation can react badly, but not unsafely.

So you can panic, abort, return dummy data, loop{}, or pretend to be pending forever, but you can't cause use-after-free, nor expose deinitialized moved-from memory.

3 Likes

Guys, I know the rules around futures and UB. I linked to them in my original post. My question is whether there is any additional guarantee for futures awaited exactly once by an async fn I control.

(It sounds like the answer is no, which is unfortunate.)

1 Like

It would likely be documented at async - Rust if it was one way or another, but I suspect it's intentionally kept unspecified because that can make certain kinds of Futures (that don't need guards to avoid UB) smaller.
(Might be a good idea to point out the unspecified behaviour explicitly there.)

It would be possible to make this guarantee with fewer downsides if poll passed something like a "Pin<&'_ own Self>"[1] through to Pending. Interestingly, I suspect that pattern could also be used to much more easily mitigate the 'Futurelock' issue discussed here: Oxide and Friends | Futurelock


  1. I take no credit for this idea, but I forgot who posted about it. Libraries can model safe owning references but not the .await sugar unless a more general trait similar to Try is added and stabilised for that. ↩︎

1 Like