async-stream icon indicating copy to clipboard operation
async-stream copied to clipboard

Soundness hole: transmuting item type using `yield` in a nested `async` block awaited in the wrong place

Open steffahn opened this issue 10 months ago • 0 comments

It seems (me looking at macro expansions) that yield usage in an async {} block is not expanded by this macro, and that’s good - in principle - because it can be unsound to await that in the wrong place with the way that thread-locals are used.

However, this is a syntactic analysis, and yield in macros are expanded, so well, that can be a problem if a macro introduces an async block:

use async_stream::stream;
use futures::StreamExt;

use std::pin::pin;

macro_rules! asynk {
    ($e:expr) => {
        async { $e }
    };
}

#[tokio::main]

async fn main() {
    pin!(stream! {
        let yield_42 = asynk!(yield 42_usize);
        let s = stream! {
            yield Box::new(12345);
            yield_42.await; // yield 42 -- wait that's not a Box!?
        };
        for await (n, i) in s.enumerate() {
            println!("Item at index {n}:\n    {i}");
            // Item at index 0:
            //     12345
            // Item at index 1:
            // Segmentation fault
        }
    })
    .next()
    .await;
}

(run on rustexplorer.com)

This kind of abuse could be better prevented with a trick I came up with in https://github.com/dureuill/nolife/issues/8#issuecomment-1975217153, the idea is to make the whole stream! invocation create a labelled block, and have yield include a if false guarded break from the same label; this will successfully prevent compilation in case the yield is anywhere where control-flow cannot directly jump back to the top-level of stream! (which should correspond to anywhere where the .await of the yield expansion does not correspond to the outer async block from the stream! expansion), hence preventing accidental use of yield in a nested async block.

(For nolife, the macro involved a macro_rules macro, where the default hygiene rules would successfully prevent the labels not matching up properly; I don't quite remember off the top of my head how far support for doing the same in proc-macros is nowadays.)

steffahn avatar Apr 22 '24 19:04 steffahn