From d39ad563d38f641a1a310e0991bc1cc495cb8e51 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 29 May 2026 13:36:38 -0700 Subject: [PATCH 1/3] wasip2: Fix zero-wait pollables This commit fixes an accidental bug introduced in #13085 where repeatedly calling `ready()` on a zero-wait pollable in WASIp2 would never resolve. In #13085 zero-length waits were updated to unconditionally yield to tokio to improve fairness, but this didn't take into account where the yield was repeatedly cancelled and never completed. The fix in this commit is to attempt the yield once and then never attempt it again. If the original yield is cancelled the next check on the pollable will go through. This is sort of a cancellation-safety fix where the previous implementation wasn't necessarily cancellation safe in the sense that repeatedly checking-and-cancelling never let anything progress, which is counterintuitive. Closes #13507 --- crates/test-programs/src/bin/p2_clocks_zero_wait.rs | 11 +++++++++++ crates/wasi/src/p2/host/clocks.rs | 8 +++++--- crates/wasi/tests/all/p2/async_.rs | 5 +++++ crates/wasi/tests/all/p2/sync.rs | 5 +++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 crates/test-programs/src/bin/p2_clocks_zero_wait.rs diff --git a/crates/test-programs/src/bin/p2_clocks_zero_wait.rs b/crates/test-programs/src/bin/p2_clocks_zero_wait.rs new file mode 100644 index 000000000000..d590514e166d --- /dev/null +++ b/crates/test-programs/src/bin/p2_clocks_zero_wait.rs @@ -0,0 +1,11 @@ +fn main() { + let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(0); + for i in 0..20 { + if pollable.ready() { + return; + } + wasip2::clocks::monotonic_clock::subscribe_duration(0).block(); + } + + panic!("pollable should eventually be ready"); +} diff --git a/crates/wasi/src/p2/host/clocks.rs b/crates/wasi/src/p2/host/clocks.rs index 3427c9cfd3e5..cb94d4536485 100644 --- a/crates/wasi/src/p2/host/clocks.rs +++ b/crates/wasi/src/p2/host/clocks.rs @@ -73,7 +73,7 @@ fn subscribe_to_duration( duration: tokio::time::Duration, ) -> wasmtime::Result> { let sleep = if duration.is_zero() { - table.push(Deadline::Past)? + table.push(Deadline::Past { yielded: false })? } else if let Some(deadline) = tokio::time::Instant::now().checked_add(duration) { // NB: this resource created here is not actually exposed to wasm, it's // only an internal implementation detail used to match the signature @@ -115,7 +115,7 @@ impl monotonic_clock::Host for WasiClocksCtxView<'_> { } enum Deadline { - Past, + Past { yielded: bool }, Instant(tokio::time::Instant), Never, } @@ -124,7 +124,8 @@ enum Deadline { impl Pollable for Deadline { async fn ready(&mut self) { match self { - Deadline::Past => { + Deadline::Past { yielded: true } => {} + Deadline::Past { yielded } => { // It is important we yield to Tokio here; otherwise we risk // starving `mio` such that it is unable to signal readiness for // other pollables (e.g. TCP sockets) when the guest is polling @@ -142,6 +143,7 @@ impl Pollable for Deadline { // are hypothetically other ways to generate a pollable that's // always immediately ready, which this hack doesn't cover, but // we consider this sufficient for now. + *yielded = true; tokio::task::yield_now().await } Deadline::Instant(instant) => tokio::time::sleep_until(*instant).await, diff --git a/crates/wasi/tests/all/p2/async_.rs b/crates/wasi/tests/all/p2/async_.rs index d8440cb8cdfa..9d1699f56c46 100644 --- a/crates/wasi/tests/all/p2/async_.rs +++ b/crates/wasi/tests/all/p2/async_.rs @@ -435,3 +435,8 @@ async fn file_truncation_readonly(component_path: &str) { let contents = std::fs::read(&file).expect("read truncation test file"); assert_eq!(EXPECTED_CONTENTS, contents); } + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p2_clocks_zero_wait() { + run(P2_CLOCKS_ZERO_WAIT_COMPONENT, |_| {}).await.unwrap() +} diff --git a/crates/wasi/tests/all/p2/sync.rs b/crates/wasi/tests/all/p2/sync.rs index e3b54159ef88..ada68d478e51 100644 --- a/crates/wasi/tests/all/p2/sync.rs +++ b/crates/wasi/tests/all/p2/sync.rs @@ -403,3 +403,8 @@ fn file_truncation_readonly(component_path: &str) { let contents = std::fs::read(&file).expect("read truncation test file"); assert_eq!(EXPECTED_CONTENTS, contents); } + +#[test_log::test] +fn p2_clocks_zero_wait() { + run(P2_CLOCKS_ZERO_WAIT_COMPONENT, |_| {}).unwrap() +} From 0dfa0bee64a08885ab7bfa0be6e858a1456e234d Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 29 May 2026 13:54:58 -0700 Subject: [PATCH 2/3] Fix compile --- crates/test-programs/src/bin/p2_clocks_zero_wait.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/test-programs/src/bin/p2_clocks_zero_wait.rs b/crates/test-programs/src/bin/p2_clocks_zero_wait.rs index d590514e166d..b3f0aeab991b 100644 --- a/crates/test-programs/src/bin/p2_clocks_zero_wait.rs +++ b/crates/test-programs/src/bin/p2_clocks_zero_wait.rs @@ -1,6 +1,6 @@ fn main() { let pollable = wasip2::clocks::monotonic_clock::subscribe_duration(0); - for i in 0..20 { + for _ in 0..20 { if pollable.ready() { return; } From 09cb4bff43d9e61d9a2c36f71cdf957e11220ddf Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 29 May 2026 15:08:53 -0700 Subject: [PATCH 3/3] Tweak tcp_busy_poll test --- crates/test-programs/src/bin/p2_tcp_busy_poll.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/test-programs/src/bin/p2_tcp_busy_poll.rs b/crates/test-programs/src/bin/p2_tcp_busy_poll.rs index 9f59886586fe..2a0f834c7c83 100644 --- a/crates/test-programs/src/bin/p2_tcp_busy_poll.rs +++ b/crates/test-programs/src/bin/p2_tcp_busy_poll.rs @@ -11,8 +11,6 @@ use test_programs::wasi::sockets::tcp::TcpSocket; // prevent e.g. socket readiness from being delivered. Here we verify that such // starvation does not happen. fn test_tcp_busy_poll(family: IpAddressFamily, address: IpSocketAddress) { - let zero_wait = monotonic_clock::subscribe_duration(0); - let net = Network::default(); let listener = TcpSocket::new(family).unwrap(); @@ -33,6 +31,7 @@ fn test_tcp_busy_poll(family: IpAddressFamily, address: IpSocketAddress) { let rx_ready = rx.subscribe(); let mut counter = 0; loop { + let zero_wait = monotonic_clock::subscribe_duration(0); if counter > 1_000_000 { panic!("socket still not ready!"); }