async-stream
async-stream copied to clipboard
Soundness hole: transmuting item type using `yield` in a nested `async` block awaited in the wrong place
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;
}
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.)