Repository: bytecodealliance/wasmtime
Pull request #8048 (2024-03-05 13:16:34)

winch: Always free the source register when emitting Wasm stores
This commit fixes a bug where when an out-of-bounds access is detected at compile time, `emit_wasm_store` was failing to free the source register. By the time the address is computed, the register is already allocated, independently if it's used or not, it must be freed.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->


github-actions[bot]: #### Subscribe to Label Action

cc @saulecabrera

<details>
This issue or pull request has been labeled: "winch"

Thus the following users have been cc'd because of the following labels:

* saulecabrera: winch

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/crates/wasi-http/src/body.rs b/crates/wasi-http/src/body.rs
index ae8e9ff9a..64819e027 100644
--- a/crates/wasi-http/src/body.rs
+++ b/crates/wasi-http/src/body.rs
@@ -10,7 +10,8 @@ use std::task::{Context, Poll};
 use std::{pin::Pin, sync::Arc, time::Duration};
 use tokio::sync::{mpsc, oneshot};
 use wasmtime_wasi::{
-    poll_noop, AbortOnDropJoinHandle, HostInputStream, HostOutputStream, StreamError, Subscribe,
+    runtime::{poll_noop, AbortOnDropJoinHandle},
+    HostInputStream, HostOutputStream, StreamError, Subscribe,
 };
 
 pub type HyperIncomingBody = BoxBody<Bytes, types::ErrorCode>;
@@ -35,7 +36,7 @@ impl BodyWithTimeout {
             inner,
             between_bytes_timeout,
             reset_sleep: true,
-            timeout: Box::pin(wasmtime_wasi::with_ambient_tokio_runtime(|| {
+            timeout: Box::pin(wasmtime_wasi::runtime::with_ambient_tokio_runtime(|| {
                 tokio::time::sleep(Duration::new(0, 0))
             })),
         }
diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs
index 5d8e9e5c4..6966ed08b 100644
--- a/crates/wasi-http/src/types.rs
+++ b/crates/wasi-http/src/types.rs
@@ -15,7 +15,7 @@ use std::time::Duration;
 use tokio::net::TcpStream;
 use tokio::time::timeout;
 use wasmtime::component::{Resource, ResourceTable};
-use wasmtime_wasi::{AbortOnDropJoinHandle, Subscribe};
+use wasmtime_wasi::{runtime::AbortOnDropJoinHandle, Subscribe};
 
 /// Capture the state necessary for use in the wasi-http API implementation.
 pub struct WasiHttpCtx;
@@ -122,7 +122,7 @@ pub fn default_send_request(
         between_bytes_timeout,
     }: OutgoingRequest,
 ) -> wasmtime::Result<Resource<HostFutureIncomingResponse>> {
-    let handle = wasmtime_wasi::spawn(async move {
+    let handle = wasmtime_wasi::runtime::spawn(async move {
         let resp = handler(
             authority,
             use_tls,
@@ -212,7 +212,7 @@ async fn handler(
             .map_err(|_| types::ErrorCode::ConnectionTimeout)?
             .map_err(hyper_request_error)?;
 
-            let worker = wasmtime_wasi::spawn(async move {
+            let worker = wasmtime_wasi::runtime::spawn(async move {
                 match conn.await {
                     Ok(()) => {}
                     // TODO: shouldn't throw away this error and ideally should
@@ -234,7 +234,7 @@ async fn handler(
         .map_err(|_| types::ErrorCode::ConnectionTimeout)?
         .map_err(hyper_request_error)?;
 
-        let worker = wasmtime_wasi::spawn(async move {
+        let worker = wasmtime_wasi::runtime::spawn(async move {
             match conn.await {
                 Ok(()) => {}
                 // TODO: same as above, shouldn't throw this error away.
diff --git a/crates/wasi/src/bindings.rs b/crates/wasi/src/bindings.rs
new file mode 100644
index 000000000..034aaa92d
--- /dev/null
+++ b/crates/wasi/src/bindings.rs
@@ -0,0 +1,120 @@
+// Generate traits for synchronous bindings.
+//
+// Note that this is only done for interfaces which can block, or those which
+// have some functions in `only_imports` below for being async.
+pub mod sync_io {
+    pub(crate) mod _internal {
+        use crate::{FsError, StreamError};
+
+        wasmtime::component::bindgen!({
+            path: "wit",
+            interfaces: "
+                    import wasi:io/poll@0.2.0;
+                    import wasi:io/streams@0.2.0;
+                    import wasi:filesystem/types@0.2.0;
+                ",
+            tracing: true,
+            trappable_error_type: {
+                "wasi:io/streams/stream-error" => StreamError,
+                "wasi:filesystem/types/error-code" => FsError,
+            },
+            with: {
+                "wasi:clocks/wall-clock": crate::bindings::clocks::wall_clock,
+                "wasi:filesystem/types/descriptor": super::super::filesystem::types::Descriptor,
+                "wasi:filesystem/types/directory-entry-stream": super::super::filesystem::types::DirectoryEntryStream,
+                "wasi:io/poll/pollable": super::super::io::poll::Pollable,
+                "wasi:io/streams/input-stream": super::super::io::streams::InputStream,
+                "wasi:io/streams/output-stream": super::super::io::streams::OutputStream,
+                "wasi:io/error/error": super::super::io::error::Error,
+            }
+        });
+    }
+    pub use self::_internal::wasi::{filesystem, io};
+}
+
+wasmtime::component::bindgen!({
+    path: "wit",
+    world: "wasi:cli/imports",
+    tracing: true,
+    async: {
+        // Only these functions are `async` and everything else is sync
+        // meaning that it basically doesn't need to block. These functions
+        // are the only ones that need to block.
+        //
+        // Note that at this time `only_imports` works on function names
+        // which in theory can be shared across interfaces, so this may
+        // need fancier syntax in the future.
+        only_imports: [
+            "[method]descriptor.access-at",
+            "[method]descriptor.advise",
+            "[method]descriptor.change-directory-permissions-at",
+            "[method]descriptor.change-file-permissions-at",
+            "[method]descriptor.create-directory-at",
+            "[method]descriptor.get-flags",
+            "[method]descriptor.get-type",
+            "[method]descriptor.is-same-object",
+            "[method]descriptor.link-at",
+            "[method]descriptor.lock-exclusive",
+            "[method]descriptor.lock-shared",
+            "[method]descriptor.metadata-hash",
+            "[method]descriptor.metadata-hash-at",
+            "[method]descriptor.open-at",
+            "[method]descriptor.read",
+            "[method]descriptor.read-directory",
+            "[method]descriptor.readlink-at",
+            "[method]descriptor.remove-directory-at",
+            "[method]descriptor.rename-at",
+            "[method]descriptor.set-size",
+            "[method]descriptor.set-times",
+            "[method]descriptor.set-times-at",
+            "[method]descriptor.stat",
+            "[method]descriptor.stat-at",
+            "[method]descriptor.symlink-at",
+            "[method]descriptor.sync",
+            "[method]descriptor.sync-data",
+            "[method]descriptor.try-lock-exclusive",
+            "[method]descriptor.try-lock-shared",
+            "[method]descriptor.unlink-file-at",
+            "[method]descriptor.unlock",
+            "[method]descriptor.write",
+            "[method]input-stream.read",
+            "[method]input-stream.blocking-read",
+            "[method]input-stream.blocking-skip",
+            "[method]input-stream.skip",
+            "[method]output-stream.forward",
+            "[method]output-stream.splice",
+            "[method]output-stream.blocking-splice",
+            "[method]output-stream.blocking-flush",
+            "[method]output-stream.blocking-write",
+            "[method]output-stream.blocking-write-and-flush",
+            "[method]output-stream.blocking-write-zeroes-and-flush",
+            "[method]directory-entry-stream.read-directory-entry",
+            "poll",
+            "[method]pollable.block",
+            "[method]pollable.ready",
+        ],
+    },
+    trappable_error_type: {
+        "wasi:io/streams/stream-error" => crate::StreamError,
+        "wasi:filesystem/types/error-code" => crate::FsError,
+        "wasi:sockets/network/error-code" => crate::SocketError,
+    },
+    with: {
+        "wasi:sockets/network/network": super::network::Network,
+        "wasi:sockets/tcp/tcp-socket": super::tcp::TcpSocket,
+        "wasi:sockets/udp/udp-socket": super::udp::UdpSocket,
+        "wasi:sockets/udp/incoming-datagram-stream": super::udp::IncomingDatagramStream,
+        "wasi:sockets/udp/outgoing-datagram-stream": super::udp::OutgoingDatagramStream,
+        "wasi:sockets/ip-name-lookup/resolve-address-stream": super::ip_name_lookup::ResolveAddressStream,
+        "wasi:filesystem/types/directory-entry-stream": super::filesystem::ReaddirIterator,
+        "wasi:filesystem/types/descriptor": super::filesystem::Descriptor,
+        "wasi:io/streams/input-stream": super::stream::InputStream,
+        "wasi:io/streams/output-stream": super::stream::OutputStream,
+        "wasi:io/error/error": super::stream::Error,
+        "wasi:io/poll/pollable": super::poll::Pollable,
+        "wasi:cli/terminal-input/terminal-input": super::stdio::TerminalInput,
+        "wasi:cli/terminal-output/terminal-output": super::stdio::TerminalOutput,
+    },
+});
+
+pub use wasi::*;
diff --git a/crates/wasi/src/filesystem.rs b/crates/wasi/src/filesystem.rs
index a71305501..86bd9398d 100644
--- a/crates/wasi/src/filesystem.rs
+++ b/crates/wasi/src/filesystem.rs
@@ -1,7 +1,6 @@
 use crate::bindings::filesystem::types;
-use crate::{
-    spawn_blocking, AbortOnDropJoinHandle, HostOutputStream, StreamError, Subscribe, TrappableError,
-};
+use crate::runtime::{spawn_blocking, AbortOnDropJoinHandle};
+use crate::{HostOutputStream, StreamError, Subscribe, TrappableError};
 use anyhow::anyhow;
 use bytes::{Bytes, BytesMut};
 use std::io;
diff --git a/crates/wasi/src/host/filesystem/sync.rs b/crates/wasi/src/host/filesystem/sync.rs
index 08b446e45..6d12e50dd 100644
--- a/crates/wasi/src/host/filesystem/sync.rs
+++ b/crates/wasi/src/host/filesystem/sync.rs
@@ -1,7 +1,8 @@
 use crate::bindings::filesystem::types as async_filesystem;
 use crate::bindings::sync_io::filesystem::types as sync_filesystem;
 use crate::bindings::sync_io::io::streams;
-use crate::{in_tokio, FsError, FsResult};
+use crate::runtime::in_tokio;
+use crate::{FsError, FsResult};
 use wasmtime::component::Resource;
 
 impl<T: async_filesystem::Host> sync_filesystem::Host for T {
diff --git a/crates/wasi/src/host/io.rs b/crates/wasi/src/host/io.rs
index 656b31bad..168088d9b 100644
--- a/crates/wasi/src/host/io.rs
+++ b/crates/wasi/src/host/io.rs
@@ -232,7 +232,8 @@ pub mod sync {
         },
         bindings::sync_io::io::poll::Pollable,
         bindings::sync_io::io::streams::{self, InputStream, OutputStream},
-        in_tokio, StreamError, StreamResult, WasiView,
+        runtime::in_tokio,
+        StreamError, StreamResult, WasiView,
     };
     use wasmtime::component::Resource;
 
diff --git a/crates/wasi/src/host/tcp.rs b/crates/wasi/src/host/tcp.rs
index 0f08862f7..11d44d882 100644
--- a/crates/wasi/src/host/tcp.rs
+++ b/crates/wasi/src/host/tcp.rs
@@ -1,5 +1,6 @@
 use crate::host::network::util;
 use crate::network::SocketAddrUse;
+use crate::runtime::with_ambient_tokio_runtime;
 use crate::tcp::{TcpReadStream, TcpSocket, TcpState, TcpWriteStream};
 use crate::{
     bindings::{
@@ -9,7 +10,7 @@ use crate::{
     },
     network::SocketAddressFamily,
 };
-use crate::{with_ambient_tokio_runtime, Pollable, SocketResult, WasiView};
+use crate::{Pollable, SocketResult, WasiView};
 use io_lifetimes::AsSocketlike;
 use rustix::io::Errno;
 use rustix::net::sockopt;
diff --git a/crates/wasi/src/ip_name_lookup.rs b/crates/wasi/src/ip_name_lookup.rs
index 85f5e8e6e..1ab3099a8 100644
--- a/crates/wasi/src/ip_name_lookup.rs
+++ b/crates/wasi/src/ip_name_lookup.rs
@@ -2,7 +2,8 @@ use crate::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
 use crate::bindings::sockets::network::{ErrorCode, IpAddress, Network};
 use crate::host::network::util;
 use crate::poll::{subscribe, Pollable, Subscribe};
-use crate::{spawn_blocking, AbortOnDropJoinHandle, SocketError, WasiView};
+use crate::runtime::{spawn_blocking, AbortOnDropJoinHandle};
+use crate::{SocketError, WasiView};
 use anyhow::Result;
 use std::mem;
 use std::net::{Ipv6Addr, ToSocketAddrs};
@@ -48,12 +49,14 @@ impl<T: WasiView> HostResolveAddressStream for T {
         let stream: &mut ResolveAddressStream = self.table().get_mut(&resource)?;
         loop {
             match stream {
-                ResolveAddressStream::Waiting(future) => match crate::poll_noop(Pin::new(future)) {
-                    Some(result) => {
-                        *stream = ResolveAddressStream::Done(result.map(|v| v.into_iter()));
+                ResolveAddressStream::Waiting(future) => {
+                    match crate::runtime::poll_noop(Pin::new(future)) {
+                        Some(result) => {
+                            *stream = ResolveAddressStream::Done(result.map(|v| v.into_iter()));
+                        }
+                        None => return Err(ErrorCode::WouldBlock.into()),
                     }
-                    None => return Err(ErrorCode::WouldBlock.into()),
-                },
+                }
                 ResolveAddressStream::Done(slot @ Err(_)) => {
                     mem::replace(slot, Ok(Vec::new().into_iter()))?;
                     unreachable!();
diff --git a/crates/wasi/src/lib.rs b/crates/wasi/src/lib.rs
index 8262d889b..225245b47 100644
--- a/crates/wasi/src/lib.rs
+++ b/crates/wasi/src/lib.rs
@@ -6,10 +6,7 @@
 //!
 //!
 
-use std::future::Future;
-use std::pin::Pin;
-use std::task::{Context, Poll};
-
+pub mod bindings;
 mod clocks;
 pub mod command;
 mod ctx;
@@ -25,6 +22,7 @@ pub mod preview0;
 #[cfg(feature = "preview1")]
 pub mod preview1;
 mod random;
+pub mod runtime;
 mod stdio;
 mod stream;
 mod tcp;
@@ -47,272 +45,3 @@ pub use self::stream::{
 pub use cap_fs_ext::SystemTimeSpec;
 pub use cap_rand::RngCore;
 pub use wasmtime::component::{ResourceTable, ResourceTableError};
-
-pub mod bindings {
-    // Generate traits for synchronous bindings.
-    //
-    // Note that this is only done for interfaces which can block, or those which
-    // have some functions in `only_imports` below for being async.
-    pub mod sync_io {
-        pub(crate) mod _internal {
-            use crate::{FsError, StreamError};
-
-            wasmtime::component::bindgen!({
-                path: "wit",
-                interfaces: "
-                    import wasi:io/poll@0.2.0;
-                    import wasi:io/streams@0.2.0;
-                    import wasi:filesystem/types@0.2.0;
-                ",
-                tracing: true,
-                trappable_error_type: {
-                    "wasi:io/streams/stream-error" => StreamError,
-                    "wasi:filesystem/types/error-code" => FsError,
-                },
-                with: {
-                    "wasi:clocks/wall-clock": crate::bindings::clocks::wall_clock,
-                    "wasi:filesystem/types/descriptor": super::super::filesystem::types::Descriptor,
-                    "wasi:filesystem/types/directory-entry-stream": super::super::filesystem::types::DirectoryEntryStream,
-                    "wasi:io/poll/pollable": super::super::io::poll::Pollable,
-                    "wasi:io/streams/input-stream": super::super::io::streams::InputStream,
-                    "wasi:io/streams/output-stream": super::super::io::streams::OutputStream,
-                    "wasi:io/error/error": super::super::io::error::Error,
-                }
-            });
-        }
-        pub use self::_internal::wasi::{filesystem, io};
-    }
-
-    wasmtime::component::bindgen!({
-        path: "wit",
-        world: "wasi:cli/imports",
-        tracing: true,
-        async: {
-            // Only these functions are `async` and everything else is sync
-            // meaning that it basically doesn't need to block. These functions
-            // are the only ones that need to block.
-            //
-            // Note that at this time `only_imports` works on function names
-            // which in theory can be shared across interfaces, so this may
-            // need fancier syntax in the future.
-            only_imports: [
-                "[method]descriptor.access-at",
-                "[method]descriptor.advise",
-                "[method]descriptor.change-directory-permissions-at",
-                "[method]descriptor.change-file-permissions-at",
-                "[method]descriptor.create-directory-at",
-                "[method]descriptor.get-flags",
-                "[method]descriptor.get-type",
-                "[method]descriptor.is-same-object",
-                "[method]descriptor.link-at",
-                "[method]descriptor.lock-exclusive",
-                "[method]descriptor.lock-shared",
-                "[method]descriptor.metadata-hash",
-                "[method]descriptor.metadata-hash-at",
-                "[method]descriptor.open-at",
-                "[method]descriptor.read",
-                "[method]descriptor.read-directory",
-                "[method]descriptor.readlink-at",
-                "[method]descriptor.remove-directory-at",
-                "[method]descriptor.rename-at",
-                "[method]descriptor.set-size",
-                "[method]descriptor.set-times",
-                "[method]descriptor.set-times-at",
-                "[method]descriptor.stat",
-                "[method]descriptor.stat-at",
-                "[method]descriptor.symlink-at",
-                "[method]descriptor.sync",
-                "[method]descriptor.sync-data",
-                "[method]descriptor.try-lock-exclusive",
-                "[method]descriptor.try-lock-shared",
-                "[method]descriptor.unlink-file-at",
-                "[method]descriptor.unlock",
-                "[method]descriptor.write",
-                "[method]input-stream.read",
-                "[method]input-stream.blocking-read",
-                "[method]input-stream.blocking-skip",
-                "[method]input-stream.skip",
-                "[method]output-stream.forward",
-                "[method]output-stream.splice",
-                "[method]output-stream.blocking-splice",
-                "[method]output-stream.blocking-flush",
-                "[method]output-stream.blocking-write",
-                "[method]output-stream.blocking-write-and-flush",
-                "[method]output-stream.blocking-write-zeroes-and-flush",
-                "[method]directory-entry-stream.read-directory-entry",
-                "poll",
-                "[method]pollable.block",
-                "[method]pollable.ready",
-            ],
-        },
-        trappable_error_type: {
-            "wasi:io/streams/stream-error" => crate::StreamError,
-            "wasi:filesystem/types/error-code" => crate::FsError,
-            "wasi:sockets/network/error-code" => crate::SocketError,
-        },
-        with: {
-            "wasi:sockets/network/network": super::network::Network,
-            "wasi:sockets/tcp/tcp-socket": super::tcp::TcpSocket,
-            "wasi:sockets/udp/udp-socket": super::udp::UdpSocket,
-            "wasi:sockets/udp/incoming-datagram-stream": super::udp::IncomingDatagramStream,
-            "wasi:sockets/udp/outgoing-datagram-stream": super::udp::OutgoingDatagramStream,
-            "wasi:sockets/ip-name-lookup/resolve-address-stream": super::ip_name_lookup::ResolveAddressStream,
-            "wasi:filesystem/types/directory-entry-stream": super::filesystem::ReaddirIterator,
-            "wasi:filesystem/types/descriptor": super::filesystem::Descriptor,
-            "wasi:io/streams/input-stream": super::stream::InputStream,
-            "wasi:io/streams/output-stream": super::stream::OutputStream,
-            "wasi:io/error/error": super::stream::Error,
-            "wasi:io/poll/pollable": super::poll::Pollable,
-            "wasi:cli/terminal-input/terminal-input": super::stdio::TerminalInput,
-            "wasi:cli/terminal-output/terminal-output": super::stdio::TerminalOutput,
-        },
-    });
-
-    pub use wasi::*;
-}
-
-pub(crate) static RUNTIME: once_cell::sync::Lazy<tokio::runtime::Runtime> =
-    once_cell::sync::Lazy::new(|| {
-        tokio::runtime::Builder::new_current_thread()
-            .enable_time()
-            .enable_io()
-            .build()
-            .unwrap()
-    });
-
-pub struct AbortOnDropJoinHandle<T>(tokio::task::JoinHandle<T>);
-impl<T> Drop for AbortOnDropJoinHandle<T> {
-    fn drop(&mut self) {
-        self.0.abort()
-    }
-}
-impl<T> std::ops::Deref for AbortOnDropJoinHandle<T> {
-    type Target = tokio::task::JoinHandle<T>;
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-impl<T> std::ops::DerefMut for AbortOnDropJoinHandle<T> {
-    fn deref_mut(&mut self) -> &mut tokio::task::JoinHandle<T> {
-        &mut self.0
-    }
-}
-impl<T> From<tokio::task::JoinHandle<T>> for AbortOnDropJoinHandle<T> {
-    fn from(jh: tokio::task::JoinHandle<T>) -> Self {
-        AbortOnDropJoinHandle(jh)
-    }
-}
-impl<T> Future for AbortOnDropJoinHandle<T> {
-    type Output = T;
-    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
-        match Pin::new(&mut self.as_mut().0).poll(cx) {
-            Poll::Pending => Poll::Pending,
-            Poll::Ready(r) => Poll::Ready(r.expect("child task panicked")),
-        }
-    }
-}
-
-pub fn spawn<F>(f: F) -> AbortOnDropJoinHandle<F::Output>
-where
-    F: Future + Send + 'static,
-    F::Output: Send + 'static,
-{
-    let j = with_ambient_tokio_runtime(|| tokio::task::spawn(f));
-    AbortOnDropJoinHandle(j)
-}
-
-pub fn spawn_blocking<F, R>(f: F) -> AbortOnDropJoinHandle<R>
-where
-    F: FnOnce() -> R + Send + 'static,
-    R: Send + 'static,
-{
-    let j = with_ambient_tokio_runtime(|| tokio::task::spawn_blocking(f));
-    AbortOnDropJoinHandle(j)
-}
-
-pub fn in_tokio<F: Future>(f: F) -> F::Output {
-    match tokio::runtime::Handle::try_current() {
-        Ok(h) => {
-            let _enter = h.enter();
-            h.block_on(f)
-        }
-        // The `yield_now` here is non-obvious and if you're reading this
-        // you're likely curious about why it's here. This is currently required
-        // to get some features of "sync mode" working correctly, such as with
-        // the CLI. To illustrate why this is required, consider a program
-        // organized as:
-        //
-        // * A program has a `pollable` that it's waiting on.
-        // * This `pollable` is always ready .
-        // * Actually making the corresponding operation ready, however,
-        //   requires some background work on Tokio's part.
-        // * The program is looping on "wait for readiness" coupled with
-        //   performing the operation.
-        //
-        // In this situation this program ends up infinitely looping in waiting
-        // for pollables. The reason appears to be that when we enter the tokio
-        // runtime here it doesn't necessary yield to background work because
-        // the provided future `f` is ready immediately. The future `f` will run
-        // through the list of pollables and determine one of them is ready.
-        //
-        // Historically this happened with UDP sockets. A test send a datagram
-        // from one socket to another and the other socket infinitely didn't
-        // receive the data. This appeared to be because the server socket was
-        // waiting on `READABLE | WRITABLE` (which is itself a bug but ignore
-        // that) and the socket was currently in the "writable" state but never
-        // ended up receiving a notification for the "readable" state. Moving
-        // the socket to "readable" would require Tokio to perform some
-        // background work via epoll/kqueue/handle events but if the future
-        // provided here is always ready, then that never happened.
-        //
-        // Thus the `yield_now()` is an attempt to force Tokio to go do some
-        // background work eventually and look at new interest masks for
-        // example. This is a bit of a kludge but everything's already a bit
-        // wonky in synchronous mode anyway. Note that this is hypothesized to
-        // not be an issue in async mode because async mode typically has the
-        // Tokio runtime in a separate thread or otherwise participating in a
-        // larger application, it's only here in synchronous mode where we
-        // effectively own the runtime that we need some special care.
-        Err(_) => {
-            let _enter = RUNTIME.enter();
-            RUNTIME.block_on(async move {
-                tokio::task::yield_now().await;
-                f.await
-            })
-        }
-    }
-}
-
-/// Executes the closure `f` with an "ambient Tokio runtime" which basically
-/// means that if code in `f` tries to get a runtime `Handle` it'll succeed.
-///
-/// If a `Handle` is already available, e.g. in async contexts, then `f` is run
-/// immediately. Otherwise for synchronous contexts this crate's fallback
-/// runtime is configured and then `f` is executed.
-pub fn with_ambient_tokio_runtime<R>(f: impl FnOnce() -> R) -> R {
-    match tokio::runtime::Handle::try_current() {
-        Ok(_) => f(),
-        Err(_) => {
-            let _enter = RUNTIME.enter();
-            f()
-        }
-    }
-}
-
-/// Attempts to get the result of a `future`.
-///
-/// This function does not block and will poll the provided future once. If the
-/// result is here then `Some` is returned, otherwise `None` is returned.
-///
-/// Note that by polling `future` this means that `future` must be re-polled
-/// later if it's to wake up a task.
-pub fn poll_noop<F>(future: Pin<&mut F>) -> Option<F::Output>
-where
-    F: Future,
-{
-    let mut task = Context::from_waker(futures::task::noop_waker_ref());
-    match future.poll(&mut task) {
-        Poll::Ready(result) => Some(result),
-        Poll::Pending => None,
-    }
-}
diff --git a/crates/wasi/src/pipe.rs b/crates/wasi/src/pipe.rs
index 48706a2c8..819455c59 100644
--- a/crates/wasi/src/pipe.rs
+++ b/crates/wasi/src/pipe.rs
@@ -112,7 +112,7 @@ pub struct AsyncReadStream {
     closed: bool,
     buffer: Option<Result<Bytes, StreamError>>,
     receiver: mpsc::Receiver<Result<Bytes, StreamError>>,
-    _join_handle: crate::AbortOnDropJoinHandle<()>,
+    _join_handle: crate::runtime::AbortOnDropJoinHandle<()>,
 }
 
 impl AsyncReadStream {
@@ -120,7 +120,7 @@ impl AsyncReadStream {
     /// provided by this struct, the argument must impl [`tokio::io::AsyncRead`].
     pub fn new<T: tokio::io::AsyncRead + Send + Unpin + 'static>(mut reader: T) -> Self {
         let (sender, receiver) = mpsc::channel(1);
-        let join_handle = crate::spawn(async move {
+        let join_handle = crate::runtime::spawn(async move {
             loop {
                 use tokio::io::AsyncReadExt;
                 let mut buf = bytes::BytesMut::with_capacity(4096);
diff --git a/crates/wasi/src/poll.rs b/crates/wasi/src/poll.rs
index 1edeae693..6d89e7fee 100644
--- a/crates/wasi/src/poll.rs
+++ b/crates/wasi/src/poll.rs
@@ -154,7 +154,8 @@ pub mod sync {
     use crate::{
         bindings::io::poll as async_poll,
         bindings::sync_io::io::poll::{self, Pollable},
-        in_tokio, WasiView,
+        runtime::in_tokio,
+        WasiView,
     };
     use anyhow::Result;
     use wasmtime::component::Resource;
diff --git a/crates/wasi/src/preview0.rs b/crates/wasi/src/preview0.rs
index 64c28aeb7..b7c8c40a7 100644
--- a/crates/wasi/src/preview0.rs
+++ b/crates/wasi/src/preview0.rs
@@ -52,7 +52,7 @@ mod sync {
     // Small wrapper around `in_tokio` to add a `Result` layer which is always
     // `Ok`
     fn in_tokio<F: Future>(future: F) -> Result<F::Output> {
-        Ok(crate::in_tokio(future))
+        Ok(crate::runtime::in_tokio(future))
     }
 }
 
diff --git a/crates/wasi/src/preview1.rs b/crates/wasi/src/preview1.rs
index 2d0f0fb7b..16a5e58ee 100644
--- a/crates/wasi/src/preview1.rs
+++ b/crates/wasi/src/preview1.rs
@@ -542,7 +542,7 @@ mod sync {
     // Small wrapper around `in_tokio` to add a `Result` layer which is always
     // `Ok`
     fn in_tokio<F: Future>(future: F) -> Result<F::Output> {
-        Ok(crate::in_tokio(future))
+        Ok(crate::runtime::in_tokio(future))
     }
 }
 
diff --git a/crates/wasi/src/runtime.rs b/crates/wasi/src/runtime.rs
new file mode 100644
index 000000000..31d8135ff
--- /dev/null
+++ b/crates/wasi/src/runtime.rs
@@ -0,0 +1,175 @@
+//! This module provides an "ambient Tokio runtime"
+//! [`with_ambient_tokio_runtime`]. Embedders of wasmtime-wasi may do so from
+//! synchronous Rust, and not use tokio directly. The implementation of
+//! wasmtime-wasi requires a tokio executor in a way that is [deeply tied to
+//! its
+//! design](https://github.com/bytecodealliance/wasmtime/issues/7973#issuecomment-1960513214).
+//! When used from a sychrnonous wasmtime context, this module provides the
+//! wrapper function [`in_tokio`] used throughout the shim implementations of
+//! synchronous component binding `Host` traits in terms of the async ones.
+//!
+//! This module also provides a thin wrapper on tokio's tasks.
+//! [`AbortOnDropJoinHandle`], which is exactly like a
+//! [`tokio::task::JoinHandle`] except for the obvious behavioral change. This
+//! whole crate, and any child crates which spawn tasks as part of their
+//! implementations, should please use this crate's [`spawn`] and
+//! [`spawn_blocking`] over tokio's. so we wanted the type name to stick out
+//! if someone misses it.
+//!
+//! Each of these facilities should be used by dependencies of wasmtime-wasi
+//! which when implementing component bindings.
+
+use std::future::Future;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+pub(crate) static RUNTIME: once_cell::sync::Lazy<tokio::runtime::Runtime> =
+    once_cell::sync::Lazy::new(|| {
+        tokio::runtime::Builder::new_current_thread()
+            .enable_time()
+            .enable_io()
+            .build()
+            .unwrap()
+    });
+
+/// Exactly like a [`tokio::task::JoinHandle`], except that it aborts the task when
+/// the handle is dropped.
+///
+/// This behavior makes it easier to tie a worker task to the lifetime of a Resource
+/// by keeping this handle owned by the Resource.
+pub struct AbortOnDropJoinHandle<T>(tokio::task::JoinHandle<T>);
+impl<T> Drop for AbortOnDropJoinHandle<T> {
+    fn drop(&mut self) {
+        self.0.abort()
+    }
+}
+impl<T> std::ops::Deref for AbortOnDropJoinHandle<T> {
+    type Target = tokio::task::JoinHandle<T>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+impl<T> std::ops::DerefMut for AbortOnDropJoinHandle<T> {
+    fn deref_mut(&mut self) -> &mut tokio::task::JoinHandle<T> {
+        &mut self.0
+    }
+}
+impl<T> From<tokio::task::JoinHandle<T>> for AbortOnDropJoinHandle<T> {
+    fn from(jh: tokio::task::JoinHandle<T>) -> Self {
+        AbortOnDropJoinHandle(jh)
+    }
+}
+impl<T> Future for AbortOnDropJoinHandle<T> {
+    type Output = T;
+    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        match Pin::new(&mut self.as_mut().0).poll(cx) {
+            Poll::Pending => Poll::Pending,
+            Poll::Ready(r) => Poll::Ready(r.expect("child task panicked")),
+        }
+    }
+}
+
+pub fn spawn<F>(f: F) -> AbortOnDropJoinHandle<F::Output>
+where
+    F: Future + Send + 'static,
+    F::Output: Send + 'static,
+{
+    let j = with_ambient_tokio_runtime(|| tokio::task::spawn(f));
+    AbortOnDropJoinHandle(j)
+}
+
+pub fn spawn_blocking<F, R>(f: F) -> AbortOnDropJoinHandle<R>
+where
+    F: FnOnce() -> R + Send + 'static,
+    R: Send + 'static,
+{
+    let j = with_ambient_tokio_runtime(|| tokio::task::spawn_blocking(f));
+    AbortOnDropJoinHandle(j)
+}
+
+pub fn in_tokio<F: Future>(f: F) -> F::Output {
+    match tokio::runtime::Handle::try_current() {
+        Ok(h) => {
+            let _enter = h.enter();
+            h.block_on(f)
+        }
+        // The `yield_now` here is non-obvious and if you're reading this
+        // you're likely curious about why it's here. This is currently required
+        // to get some features of "sync mode" working correctly, such as with
+        // the CLI. To illustrate why this is required, consider a program
+        // organized as:
+        //
+        // * A program has a `pollable` that it's waiting on.
+        // * This `pollable` is always ready .
+        // * Actually making the corresponding operation ready, however,
+        //   requires some background work on Tokio's part.
+        // * The program is looping on "wait for readiness" coupled with
+        //   performing the operation.
+        //
+        // In this situation this program ends up infinitely looping in waiting
+        // for pollables. The reason appears to be that when we enter the tokio
+        // runtime here it doesn't necessary yield to background work because
+        // the provided future `f` is ready immediately. The future `f` will run
+        // through the list of pollables and determine one of them is ready.
+        //
+        // Historically this happened with UDP sockets. A test send a datagram
+        // from one socket to another and the other socket infinitely didn't
+        // receive the data. This appeared to be because the server socket was
+        // waiting on `READABLE | WRITABLE` (which is itself a bug but ignore
+        // that) and the socket was currently in the "writable" state but never
+        // ended up receiving a notification for the "readable" state. Moving
+        // the socket to "readable" would require Tokio to perform some
+        // background work via epoll/kqueue/handle events but if the future
+        // provided here is always ready, then that never happened.
+        //
+        // Thus the `yield_now()` is an attempt to force Tokio to go do some
+        // background work eventually and look at new interest masks for
+        // example. This is a bit of a kludge but everything's already a bit
+        // wonky in synchronous mode anyway. Note that this is hypothesized to
+        // not be an issue in async mode because async mode typically has the
+        // Tokio runtime in a separate thread or otherwise participating in a
+        // larger application, it's only here in synchronous mode where we
+        // effectively own the runtime that we need some special care.
+        Err(_) => {
+            let _enter = RUNTIME.enter();
+            RUNTIME.block_on(async move {
+                tokio::task::yield_now().await;
+                f.await
+            })
+        }
+    }
+}
+
+/// Executes the closure `f` with an "ambient Tokio runtime" which basically
+/// means that if code in `f` tries to get a runtime `Handle` it'll succeed.
+///
+/// If a `Handle` is already available, e.g. in async contexts, then `f` is run
+/// immediately. Otherwise for synchronous contexts this crate's fallback
+/// runtime is configured and then `f` is executed.
+pub fn with_ambient_tokio_runtime<R>(f: impl FnOnce() -> R) -> R {
+    match tokio::runtime::Handle::try_current() {
+        Ok(_) => f(),
+        Err(_) => {
+            let _enter = RUNTIME.enter();
+            f()
+        }
+    }
+}
+
+/// Attempts to get the result of a `future`.
+///
+/// This function does not block and will poll the provided future once. If the
+/// result is here then `Some` is returned, otherwise `None` is returned.
+///
+/// Note that by polling `future` this means that `future` must be re-polled
+/// later if it's to wake up a task.
+pub fn poll_noop<F>(future: Pin<&mut F>) -> Option<F::Output>
+where
+    F: Future,
+{
+    let mut task = Context::from_waker(futures::task::noop_waker_ref());
+    match future.poll(&mut task) {
+        Poll::Ready(result) => Some(result),
+        Poll::Pending => None,
+    }
+}
diff --git a/crates/wasi/src/tcp.rs b/crates/wasi/src/tcp.rs
index 468bbb75c..bc970c3bf 100644
--- a/crates/wasi/src/tcp.rs
+++ b/crates/wasi/src/tcp.rs
@@ -1,8 +1,6 @@
-use super::network::SocketAddressFamily;
-use super::{
-    with_ambient_tokio_runtime, HostInputStream, HostOutputStream, SocketResult, StreamError,
-};
-use crate::{AbortOnDropJoinHandle, Subscribe};
+use crate::network::SocketAddressFamily;
+use crate::runtime::{with_ambient_tokio_runtime, AbortOnDropJoinHandle};
+use crate::{HostInputStream, HostOutputStream, SocketResult, StreamError, Subscribe};
 use anyhow::{Error, Result};
 use cap_net_ext::AddressFamily;
 use futures::Future;
@@ -164,7 +162,7 @@ impl TcpWriteStream {
         assert!(matches!(self.last_write, LastWrite::Done));
 
         let stream = self.stream.clone();
-        self.last_write = LastWrite::Waiting(crate::spawn(async move {
+        self.last_write = LastWrite::Waiting(crate::runtime::spawn(async move {
             // Note: we are not using the AsyncWrite impl here, and instead using the TcpStream
             // primitive try_write, which goes directly to attempt a write with mio. This has
             // two advantages: 1. this operation takes a &TcpStream instead of a &mut TcpStream
@@ -236,7 +234,7 @@ impl HostOutputStream for TcpWriteStream {
 
         let writable = self.stream.writable();
         futures::pin_mut!(writable);
-        if super::poll_noop(writable).is_none() {
+        if crate::runtime::poll_noop(writable).is_none() {
             return Ok(0);
         }
         Ok(SOCKET_READY_SIZE)
diff --git a/crates/wasi/src/udp.rs b/crates/wasi/src/udp.rs
index bb9819f2e..1e0b1a199 100644
--- a/crates/wasi/src/udp.rs
+++ b/crates/wasi/src/udp.rs
@@ -1,6 +1,6 @@
 use crate::host::network::util;
 use crate::poll::Subscribe;
-use crate::with_ambient_tokio_runtime;
+use crate::runtime::with_ambient_tokio_runtime;
 use async_trait::async_trait;
 use cap_net_ext::{AddressFamily, Blocking};
 use io_lifetimes::raw::{FromRawSocketlike, IntoRawSocketlike};
diff --git a/crates/wasi/src/write_stream.rs b/crates/wasi/src/write_stream.rs
index eb76e2c3b..e08546b60 100644
--- a/crates/wasi/src/write_stream.rs
+++ b/crates/wasi/src/write_stream.rs
@@ -139,7 +139,7 @@ impl Worker {
 /// Provides a [`HostOutputStream`] impl from a [`tokio::io::AsyncWrite`] impl
 pub struct AsyncWriteStream {
     worker: Arc<Worker>,
-    _join_handle: crate::AbortOnDropJoinHandle<()>,
+    _join_handle: crate::runtime::AbortOnDropJoinHandle<()>,
 }
 
 impl AsyncWriteStream {
@@ -152,7 +152,7 @@ impl AsyncWriteStream {
         let worker = Arc::new(Worker::new(write_budget));
 
         let w = Arc::clone(&worker);
-        let join_handle = crate::spawn(async move { w.work(writer).await });
+        let join_handle = crate::runtime::spawn(async move { w.work(writer).await });
 
         AsyncWriteStream {
             worker,
diff --git a/winch/codegen/src/codegen/mod.rs b/winch/codegen/src/codegen/mod.rs
index 8e933ed74..475c7f74d 100644
--- a/winch/codegen/src/codegen/mod.rs
+++ b/winch/codegen/src/codegen/mod.rs
@@ -658,8 +658,8 @@ where
                 .wasm_store(src.reg.into(), self.masm.address_at_reg(addr, 0), size);
 
             self.context.free_reg(addr);
-            self.context.free_reg(src);
         }
+        self.context.free_reg(src);
     }
 }
 


Test patch:
diff --git a/crates/wasi-http/tests/all/main.rs b/crates/wasi-http/tests/all/main.rs
index af1a4d457..800f3a0bc 100644
--- a/crates/wasi-http/tests/all/main.rs
+++ b/crates/wasi-http/tests/all/main.rs
@@ -170,7 +170,7 @@ async fn run_wasi_http(
     let (sender, receiver) = tokio::sync::oneshot::channel();
     let out = store.data_mut().new_response_outparam(sender)?;
 
-    let handle = wasmtime_wasi::spawn(async move {
+    let handle = wasmtime_wasi::runtime::spawn(async move {
         proxy
             .wasi_http_incoming_handler()
             .call_handle(&mut store, req, out)
@@ -278,7 +278,7 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> {
                 let response = handle(request.into_parts().0).map(|resp| {
                     Ok(IncomingResponseInternal {
                         resp,
-                        worker: Arc::new(wasmtime_wasi::spawn(future::ready(()))),
+                        worker: Arc::new(wasmtime_wasi::runtime::spawn(future::ready(()))),
                         between_bytes_timeout,
                     })
                 });
diff --git a/tests/all/winch.rs b/tests/all/winch.rs
index fc8a66744..addfc2783 100644
--- a/tests/all/winch.rs
+++ b/tests/all/winch.rs
@@ -310,3 +310,33 @@ fn dynamic_heap() -> Result<()> {
 
     Ok(())
 }
+
+#[test]
+#[cfg_attr(miri, ignore)]
+#[cfg_attr(windows, ignore)]
+fn static_oob() -> Result<()> {
+    let mut c = Config::new();
+    c.static_memory_maximum_size(65536);
+    c.strategy(Strategy::Winch);
+    let engine = Engine::new(&c)?;
+    let wat = r#"
+        (module
+          (memory 0 1)
+          (func (export "") (result i32)
+            (i32.const 0)
+            (i32.const 1)
+            (i32.store offset=726020653)
+            (i32.const 1)
+            (memory.grow)
+          )
+        )
+    "#;
+    let mut store = Store::new(&engine, ());
+    let module = Module::new(&engine, wat)?;
+    let instance = Instance::new(&mut store, &module, &[])?;
+    let f = instance.get_typed_func::<(), i32>(&mut store, "")?;
+    let result = f.call(&mut store, ()).unwrap_err();
+    assert!(result.downcast_ref::<WasmBacktrace>().is_some());
+
+    Ok(())
+}