Skip to content

Commit 1e1aabf

Browse files
committed
Synchronize with destroying spawned tasks more in wasip2
This commit fixes a test failure I'm running into in wasi-libc development where spawned tasks for wasip2 are keeping objects alive in a race condition where sometimes the task is torn down and sometimes it's not. Specifically wasip2 is built on wasip3-style primitives for UDP/TCP which means that futures are used, and if futures aren't immediately ready they're resolved in a spawned Tokio task. This spawned task can interact with UDP, for example, where exclusivity of a UDP socket is tested via `Arc::get_mut` which will nondeterministically return true or false depending if a previously spawned task has exited or not. In wasi-libc this means that reconnecting a UDP socket sometimes fails and sometimes passes because the background task may or may not have exited. Here this is resolved by making the `drop` methods async and then hooking into the preexisting `cancel` method which aborts the task and then waits on the result. This synchronizes with the task to ensure that the state of the socket is guaranteed to be exclusive after a disconnect and ready for another connect. This similar fix is then applied to ip-name-lookup as well.
1 parent b874bfa commit 1e1aabf

7 files changed

Lines changed: 56 additions & 30 deletions

File tree

crates/wasi/src/p2/bindings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,8 @@ mod async_io {
371371
"wasi:sockets/udp.[method]udp-socket.start-bind": async | tracing | trappable,
372372
"wasi:sockets/udp.[method]udp-socket.stream": async | tracing | trappable,
373373
"wasi:sockets/udp.[drop]outgoing-datagram-stream": async | tracing | trappable,
374+
"wasi:sockets/udp.[drop]incoming-datagram-stream": async | tracing | trappable,
375+
"wasi:sockets/ip-name-lookup.[drop]resolve-address-stream": async | tracing | trappable,
374376
default: tracing | trappable,
375377
},
376378
exports: { default: async },

crates/wasi/src/p2/host/udp.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,15 @@ impl udp::HostIncomingDatagramStream for WasiSocketsCtxView<'_> {
217217
wasmtime_wasi_io::poll::subscribe(self.table, this)
218218
}
219219

220-
fn drop(&mut self, this: Resource<udp::IncomingDatagramStream>) -> Result<(), wasmtime::Error> {
220+
async fn drop(
221+
&mut self,
222+
this: Resource<udp::IncomingDatagramStream>,
223+
) -> Result<(), wasmtime::Error> {
221224
// As in the filesystem implementation, we assume closing a socket
222225
// doesn't block.
223226
let dropped = self.table.delete(this)?;
224-
drop(dropped);
227+
228+
dropped.finish().await;
225229

226230
Ok(())
227231
}
@@ -481,7 +485,7 @@ pub mod sync {
481485
}
482486

483487
fn drop(&mut self, rep: Resource<IncomingDatagramStream>) -> wasmtime::Result<()> {
484-
AsyncHostIncomingDatagramStream::drop(self, rep)
488+
in_tokio(async { AsyncHostIncomingDatagramStream::drop(self, rep).await })
485489
}
486490
}
487491

crates/wasi/src/p2/ip_name_lookup.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ use crate::p2::SocketError;
22
use crate::p2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
33
use crate::p2::bindings::sockets::network::{ErrorCode, IpAddress, Network};
44
use crate::sockets::ip_name_lookup::resolve_addresses;
5-
use crate::sockets::{MaybeReady, WasiSocketsCtxView, noop_cx};
5+
use crate::sockets::{MaybeSpawned, WasiSocketsCtxView, noop_cx};
66
use std::net::IpAddr;
77
use std::task::Poll;
88
use std::vec;
99
use wasmtime::Result;
1010
use wasmtime::component::Resource;
1111
use wasmtime_wasi_io::poll::{DynPollable, Pollable, subscribe};
1212

13-
pub struct ResolveAddressStream(MaybeReady<Result<vec::IntoIter<IpAddr>, ErrorCode>>);
13+
pub struct ResolveAddressStream(MaybeSpawned<Result<vec::IntoIter<IpAddr>, ErrorCode>>);
1414

1515
impl Host for WasiSocketsCtxView<'_> {
1616
fn resolve_addresses(
@@ -24,12 +24,12 @@ impl Host for WasiSocketsCtxView<'_> {
2424
_ = self.table.get(&network)?;
2525

2626
let fut = resolve_addresses(&self.ctx, name);
27-
let stream = ResolveAddressStream(MaybeReady::poll_or_spawn(async move {
27+
let stream = ResolveAddressStream(MaybeSpawned::poll_or_spawn(async move {
2828
Ok(fut.await?.into_iter())
2929
}));
3030

3131
// Attempt to surface errors immediately.
32-
if let MaybeReady::Ready(Err(err)) = &stream.0 {
32+
if let MaybeSpawned::Ready(Err(err)) = &stream.0 {
3333
return Err((*err).into());
3434
}
3535
Ok(self.table.push(stream)?)
@@ -59,8 +59,11 @@ impl HostResolveAddressStream for WasiSocketsCtxView<'_> {
5959
subscribe(self.table, resource)
6060
}
6161

62-
fn drop(&mut self, resource: Resource<ResolveAddressStream>) -> Result<()> {
63-
self.table.delete(resource)?;
62+
async fn drop(&mut self, resource: Resource<ResolveAddressStream>) -> Result<()> {
63+
let stream = self.table.delete(resource)?;
64+
if let MaybeSpawned::Pending(fut) = stream.0 {
65+
fut.cancel().await;
66+
}
6467
Ok(())
6568
}
6669
}

crates/wasi/src/p2/tcp.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::p2::{
44
DynInputStream, DynOutputStream, InputStream, OutputStream, Pollable, SocketResult, StreamError,
55
};
66
use crate::sockets::{
7-
MaybeReady, TcpListenStream, TcpReceiveStream, TcpSendStream, TcpSocket as P3Socket, noop_cx,
7+
MaybeSpawned, TcpListenStream, TcpReceiveStream, TcpSendStream, TcpSocket as P3Socket, noop_cx,
88
};
99
use std::future::poll_fn;
1010
use std::mem;
@@ -147,8 +147,8 @@ impl From<WriteError> for StreamError {
147147

148148
enum WriteState {
149149
Ready(TcpSendStream, usize),
150-
Writing(MaybeReady<Result<TcpSendStream, WriteError>>),
151-
Closing(MaybeReady<Result<(), WriteError>>),
150+
Writing(MaybeSpawned<Result<TcpSendStream, WriteError>>),
151+
Closing(MaybeSpawned<Result<(), WriteError>>),
152152
Closed(WriteError),
153153
}
154154

@@ -191,7 +191,7 @@ impl WriteState {
191191
}
192192
};
193193

194-
*self = WriteState::Writing(MaybeReady::poll_or_spawn(async move {
194+
*self = WriteState::Writing(MaybeSpawned::poll_or_spawn(async move {
195195
while !bytes.is_empty() {
196196
match stream.write(&bytes).await {
197197
Ok(n) => {
@@ -233,7 +233,7 @@ impl WriteState {
233233

234234
// Schedule the shutdown after the current write has finished:
235235
WriteState::Writing(write) => {
236-
WriteState::Closing(MaybeReady::poll_or_spawn(async move {
236+
WriteState::Closing(MaybeSpawned::poll_or_spawn(async move {
237237
_ = write.into_future().await?;
238238
Ok(())
239239
}))

crates/wasi/src/p2/udp.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use futures::TryFutureExt;
22

33
use crate::{
44
p2::bindings::sockets::network::ErrorCode,
5-
sockets::{MaybeReady, UdpSocket as P3Socket, noop_cx},
5+
sockets::{MaybeSpawned, UdpSocket as P3Socket, noop_cx},
66
};
77
use std::{
88
net::SocketAddr,
@@ -37,7 +37,7 @@ pub(crate) enum AsyncOperation {
3737
pub struct IncomingDatagramStream {
3838
pub(crate) inner: Arc<Mutex<P3Socket>>,
3939
pub(crate) connected_addr: Option<SocketAddr>,
40-
pub(crate) current_recv: Option<MaybeReady<Result<(Vec<u8>, SocketAddr), ErrorCode>>>,
40+
pub(crate) current_recv: Option<MaybeSpawned<Result<(Vec<u8>, SocketAddr), ErrorCode>>>,
4141
}
4242
impl IncomingDatagramStream {
4343
pub(crate) fn new(inner: Arc<Mutex<P3Socket>>) -> Self {
@@ -55,7 +55,7 @@ impl IncomingDatagramStream {
5555
if self.current_recv.is_none() {
5656
let connected_addr = self.connected_addr;
5757
let inner = self.inner.clone();
58-
let recv = MaybeReady::poll_or_spawn(async move {
58+
let recv = MaybeSpawned::poll_or_spawn(async move {
5959
loop {
6060
let fut = inner.lock().unwrap().recv();
6161
let (data, addr) = fut.await?;
@@ -96,13 +96,20 @@ impl IncomingDatagramStream {
9696

9797
self.current_recv.take().unwrap().unwrap_ready()
9898
}
99+
100+
pub(crate) async fn finish(mut self) {
101+
let Some(MaybeSpawned::Pending(recv)) = self.current_recv.take() else {
102+
return;
103+
};
104+
recv.cancel().await;
105+
}
99106
}
100107

101108
pub struct OutgoingDatagramStream {
102109
pub(crate) inner: Arc<Mutex<P3Socket>>,
103110
/// Number of datagrams permitted by most recent `check-send` call.
104111
pub(crate) check_send_permit_count: usize,
105-
pub(crate) prev_send: Option<MaybeReady<Result<(), ErrorCode>>>,
112+
pub(crate) prev_send: Option<MaybeSpawned<Result<(), ErrorCode>>>,
106113
}
107114
impl OutgoingDatagramStream {
108115
pub(crate) fn new(inner: Arc<Mutex<P3Socket>>) -> Self {
@@ -140,7 +147,7 @@ impl OutgoingDatagramStream {
140147

141148
debug_assert!(self.prev_send.is_none());
142149

143-
let mut send = MaybeReady::poll_or_spawn(
150+
let mut send = MaybeSpawned::poll_or_spawn(
144151
self.inner
145152
.lock()
146153
.unwrap()

crates/wasi/src/sockets/mod.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::runtime::AbortOnDropJoinHandle;
12
use core::fmt;
23
use core::future::Future;
34
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
@@ -231,15 +232,14 @@ pub(crate) enum SocketAddressFamily {
231232
/// (1) polling a future for completion and
232233
/// (2) obtaining the output of a future
233234
/// into separate operations. This is a common pattern in WASI 0.2.
234-
pub(crate) enum MaybeReady<T> {
235-
Pending(Pin<Box<dyn Future<Output = T> + Send>>),
235+
pub(crate) enum MaybeReady<T, F = Pin<Box<dyn Future<Output = T> + Send>>> {
236+
Pending(F),
236237
Ready(T),
237238
}
238-
impl<T> MaybeReady<T> {
239-
pub(crate) fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
240-
Self::Pending(Box::pin(fut))
241-
}
242239

240+
pub(crate) type MaybeSpawned<T> = MaybeReady<T, AbortOnDropJoinHandle<T>>;
241+
242+
impl<T> MaybeSpawned<T> {
243243
/// Poll the future and attempt to resolve it immediately. If the future is
244244
/// not ready yet, it will be moved to a background task.
245245
pub(crate) fn poll_or_spawn(fut: impl Future<Output = T> + Send + 'static) -> Self
@@ -252,6 +252,16 @@ impl<T> MaybeReady<T> {
252252
Poll::Pending => Self::new(crate::runtime::spawn(fut)),
253253
}
254254
}
255+
}
256+
257+
impl<T, F> MaybeReady<T, F>
258+
where
259+
F: Future<Output = T> + Unpin,
260+
{
261+
pub(crate) fn new(fut: F) -> Self {
262+
Self::Pending(fut)
263+
}
264+
255265
pub(crate) fn unwrap_ready(self) -> T {
256266
match self {
257267
Self::Ready(val) => val,
@@ -260,7 +270,7 @@ impl<T> MaybeReady<T> {
260270
}
261271
pub(crate) fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<&mut T> {
262272
match self {
263-
Self::Pending(fut) => match fut.as_mut().poll(cx) {
273+
Self::Pending(fut) => match Pin::new(fut).as_mut().poll(cx) {
264274
Poll::Ready(val) => {
265275
*self = Self::Ready(val);
266276
Poll::Ready(match self {

crates/wasi/src/sockets/tcp.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ impl TcpSocket {
191191
unreachable!();
192192
};
193193

194-
self.tcp_state = TcpState::Connecting(MaybeReady::new(async move {
194+
self.tcp_state = TcpState::Connecting(MaybeReady::new(Box::pin(async move {
195195
// Perform all checks before doing any syscalls.
196196
{
197197
if !already_bound {
@@ -213,7 +213,7 @@ impl TcpSocket {
213213

214214
let stream = sock.connect(addr).await?;
215215
Ok(stream)
216-
}));
216+
})));
217217

218218
Ok(())
219219
}
@@ -569,7 +569,7 @@ impl TcpListenStream {
569569
let listener = self.inner.clone();
570570
let permissions = self.permissions.clone();
571571

572-
self.pending_accept = Some(MaybeReady::new(async move {
572+
self.pending_accept = Some(MaybeReady::new(Box::pin(async move {
573573
loop {
574574
match accept(&listener).await {
575575
Ok((client, addr)) => {
@@ -589,7 +589,7 @@ impl TcpListenStream {
589589
}
590590
}
591591
}
592-
}));
592+
})));
593593
}
594594

595595
with_ambient_tokio_runtime(|| {

0 commit comments

Comments
 (0)