Skip to content

Commit cfbaf82

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 7d4c8b4 commit cfbaf82

9 files changed

Lines changed: 126 additions & 35 deletions

File tree

crates/test-programs/src/bin/p2_udp_connect.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,12 +154,36 @@ fn test_udp_connect_and_send(net: &Network, family: IpAddressFamily) {
154154
));
155155
}
156156

157+
fn test_udp_reconnect_after_pending_receive(net: &Network, family: IpAddressFamily) {
158+
let unspecified_addr = IpSocketAddress::new(IpAddress::new_unspecified(family), 0);
159+
let remote = IpSocketAddress::new(IpAddress::new_loopback(family), 4321);
160+
161+
let client = UdpSocket::new(family).unwrap();
162+
client.blocking_bind(&net, unspecified_addr).unwrap();
163+
164+
// Connect/reconnect in a loop, and this should always succeed...
165+
for _ in 0..100 {
166+
{
167+
let (rx, _tx) = client.stream(None).unwrap();
168+
assert!(rx.receive(1).unwrap().is_empty());
169+
}
170+
{
171+
let (rx, _tx) = client.stream(Some(remote)).unwrap();
172+
assert_eq!(client.remote_address(), Ok(remote));
173+
assert!(rx.receive(1).unwrap().is_empty());
174+
}
175+
}
176+
}
177+
157178
fn main() {
158179
let net = Network::default();
159180

160181
test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv4);
161182
test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv6);
162183

184+
test_udp_reconnect_after_pending_receive(&net, IpAddressFamily::Ipv4);
185+
test_udp_reconnect_after_pending_receive(&net, IpAddressFamily::Ipv6);
186+
163187
test_udp_disconnect_local_address(&net, IpAddressFamily::Ipv4);
164188
test_udp_disconnect_local_address(&net, IpAddressFamily::Ipv6);
165189

crates/wasi/src/p2/bindings.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ pub mod sync {
172172
"wasi:sockets/udp.incoming-datagram-stream": super::super::sockets::udp::IncomingDatagramStream,
173173
"wasi:sockets/udp.outgoing-datagram-stream": super::super::sockets::udp::OutgoingDatagramStream,
174174
"wasi:sockets/udp.udp-socket": crate::p2::UdpSocket,
175+
"wasi:sockets/ip-name-lookup.resolve-address-stream": crate::p2::ip_name_lookup::ResolveAddressStream,
175176

176177
// Error host trait from wasmtime-wasi-io is synchronous, so we can alias it
177178
"wasi:io/error": wasmtime_wasi_io::bindings::wasi::io::error,
@@ -371,6 +372,8 @@ mod async_io {
371372
"wasi:sockets/udp.[method]udp-socket.start-bind": async | tracing | trappable,
372373
"wasi:sockets/udp.[method]udp-socket.stream": async | tracing | trappable,
373374
"wasi:sockets/udp.[drop]outgoing-datagram-stream": async | tracing | trappable,
375+
"wasi:sockets/udp.[drop]incoming-datagram-stream": async | tracing | trappable,
376+
"wasi:sockets/ip-name-lookup.[drop]resolve-address-stream": async | tracing | trappable,
374377
default: tracing | trappable,
375378
},
376379
exports: { default: async },

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,12 @@ 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> {
221-
// As in the filesystem implementation, we assume closing a socket
222-
// doesn't block.
220+
async fn drop(
221+
&mut self,
222+
this: Resource<udp::IncomingDatagramStream>,
223+
) -> Result<(), wasmtime::Error> {
223224
let dropped = self.table.delete(this)?;
224-
drop(dropped);
225-
225+
dropped.finish().await;
226226
Ok(())
227227
}
228228
}
@@ -479,7 +479,7 @@ pub mod sync {
479479
}
480480

481481
fn drop(&mut self, rep: Resource<IncomingDatagramStream>) -> wasmtime::Result<()> {
482-
AsyncHostIncomingDatagramStream::drop(self, rep)
482+
in_tokio(async { AsyncHostIncomingDatagramStream::drop(self, rep).await })
483483
}
484484
}
485485

crates/wasi/src/p2/ip_name_lookup.rs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ use crate::p2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStrea
33
use crate::p2::bindings::sockets::network::{ErrorCode, IpAddress, Network};
44
use crate::runtime::poll_now;
55
use crate::sockets::ip_name_lookup::resolve_addresses;
6-
use crate::sockets::{MaybeReady, WasiSocketsCtxView};
6+
use crate::sockets::{MaybeSpawned, WasiSocketsCtxView};
77
use std::net::IpAddr;
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
}
@@ -71,3 +74,47 @@ impl Pollable for ResolveAddressStream {
7174
std::future::poll_fn(|cx| self.0.poll_ready(cx).map(|_| ())).await
7275
}
7376
}
77+
78+
mod sync {
79+
use super::ResolveAddressStream;
80+
use crate::p2::SocketError;
81+
use crate::p2::bindings::sockets::network::{IpAddress, Network};
82+
use crate::p2::bindings::sync::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
83+
use crate::runtime::in_tokio;
84+
use crate::sockets::WasiSocketsCtxView;
85+
use wasmtime::Result;
86+
use wasmtime::component::Resource;
87+
use wasmtime_wasi_io::poll::DynPollable;
88+
89+
impl Host for WasiSocketsCtxView<'_> {
90+
fn resolve_addresses(
91+
&mut self,
92+
network: Resource<Network>,
93+
name: String,
94+
) -> Result<Resource<ResolveAddressStream>, SocketError> {
95+
<Self as super::Host>::resolve_addresses(self, network, name)
96+
}
97+
}
98+
99+
impl HostResolveAddressStream for WasiSocketsCtxView<'_> {
100+
fn resolve_next_address(
101+
&mut self,
102+
resource: Resource<ResolveAddressStream>,
103+
) -> Result<Option<IpAddress>, SocketError> {
104+
<Self as super::HostResolveAddressStream>::resolve_next_address(self, resource)
105+
}
106+
107+
fn subscribe(
108+
&mut self,
109+
resource: Resource<ResolveAddressStream>,
110+
) -> Result<Resource<DynPollable>> {
111+
<Self as super::HostResolveAddressStream>::subscribe(self, resource)
112+
}
113+
114+
fn drop(&mut self, resource: Resource<ResolveAddressStream>) -> Result<()> {
115+
in_tokio(<Self as super::HostResolveAddressStream>::drop(
116+
self, resource,
117+
))
118+
}
119+
}
120+
}

crates/wasi/src/p2/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ pub fn add_to_linker_with_options_async<T: WasiView>(
329329
bindings::sockets::tcp::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
330330
bindings::sockets::udp::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
331331
bindings::sockets::udp_create_socket::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
332+
bindings::sockets::ip_name_lookup::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
332333
Ok(())
333334
}
334335

@@ -362,7 +363,6 @@ where
362363
sockets::tcp_create_socket::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
363364
sockets::instance_network::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
364365
sockets::network::add_to_linker::<T, WasiSockets>(l, &options.into(), T::sockets)?;
365-
sockets::ip_name_lookup::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
366366
Ok(())
367367
}
368368

@@ -469,6 +469,7 @@ pub fn add_to_linker_with_options_sync<T: WasiView>(
469469
bindings::sync::sockets::tcp::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
470470
bindings::sync::sockets::udp::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
471471
bindings::sync::sockets::udp_create_socket::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
472+
bindings::sync::sockets::ip_name_lookup::add_to_linker::<T, WasiSockets>(l, T::sockets)?;
472473
Ok(())
473474
}
474475

crates/wasi/src/p2/tcp.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::p2::{
55
};
66
use crate::runtime::poll_now;
77
use crate::sockets::{
8-
MaybeReady, TcpListenStream, TcpReceiveStream, TcpSendStream, TcpSocket as P3Socket,
8+
MaybeSpawned, TcpListenStream, TcpReceiveStream, TcpSendStream, TcpSocket as P3Socket,
99
};
1010
use std::future::poll_fn;
1111
use std::mem;
@@ -148,8 +148,8 @@ impl From<WriteError> for StreamError {
148148

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

@@ -192,7 +192,7 @@ impl WriteState {
192192
}
193193
};
194194

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

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

crates/wasi/src/p2/udp.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use futures::TryFutureExt;
33
use crate::{
44
p2::bindings::sockets::network::ErrorCode,
55
runtime::poll_now,
6-
sockets::{MaybeReady, UdpSocket as P3Socket},
6+
sockets::{MaybeSpawned, UdpSocket as P3Socket},
77
};
88
use std::{
99
net::SocketAddr,
@@ -38,7 +38,7 @@ pub(crate) enum AsyncOperation {
3838
pub struct IncomingDatagramStream {
3939
pub(crate) inner: Arc<Mutex<P3Socket>>,
4040
pub(crate) connected_addr: Option<SocketAddr>,
41-
pub(crate) current_recv: Option<MaybeReady<Result<(Vec<u8>, SocketAddr), ErrorCode>>>,
41+
pub(crate) current_recv: Option<MaybeSpawned<Result<(Vec<u8>, SocketAddr), ErrorCode>>>,
4242
}
4343
impl IncomingDatagramStream {
4444
pub(crate) fn new(inner: Arc<Mutex<P3Socket>>) -> Self {
@@ -56,7 +56,7 @@ impl IncomingDatagramStream {
5656
if self.current_recv.is_none() {
5757
let connected_addr = self.connected_addr;
5858
let inner = self.inner.clone();
59-
let recv = MaybeReady::poll_or_spawn(async move {
59+
let recv = MaybeSpawned::poll_or_spawn(async move {
6060
loop {
6161
let fut = inner.lock().unwrap().recv();
6262
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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::runtime::poll_noop;
1+
use crate::runtime::{AbortOnDropJoinHandle, poll_noop};
22
use core::fmt;
33
use core::future::Future;
44
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
@@ -232,15 +232,14 @@ pub(crate) enum SocketAddressFamily {
232232
/// (1) polling a future for completion and
233233
/// (2) obtaining the output of a future
234234
/// into separate operations. This is a common pattern in WASI 0.2.
235-
pub(crate) enum MaybeReady<T> {
236-
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),
237237
Ready(T),
238238
}
239-
impl<T> MaybeReady<T> {
240-
pub(crate) fn new(fut: impl Future<Output = T> + Send + 'static) -> Self {
241-
Self::Pending(Box::pin(fut))
242-
}
243239

240+
pub(crate) type MaybeSpawned<T> = MaybeReady<T, AbortOnDropJoinHandle<T>>;
241+
242+
impl<T> MaybeSpawned<T> {
244243
/// Poll the future and attempt to resolve it immediately. If the future is
245244
/// not ready yet, it will be moved to a background task.
246245
pub(crate) fn poll_or_spawn(fut: impl Future<Output = T> + Send + 'static) -> Self
@@ -253,6 +252,16 @@ impl<T> MaybeReady<T> {
253252
None => Self::new(crate::runtime::spawn(fut)),
254253
}
255254
}
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+
256265
pub(crate) fn unwrap_ready(self) -> T {
257266
match self {
258267
Self::Ready(val) => val,
@@ -261,7 +270,7 @@ impl<T> MaybeReady<T> {
261270
}
262271
pub(crate) fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<&mut T> {
263272
match self {
264-
Self::Pending(fut) => match fut.as_mut().poll(cx) {
273+
Self::Pending(fut) => match Pin::new(fut).as_mut().poll(cx) {
265274
Poll::Ready(val) => {
266275
*self = Self::Ready(val);
267276
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)