Repository: bytecodealliance/wasmtime
Pull request #7358 (2023-10-25 00:55:30)

monotonic clock: introduce `duration` type, split `subscribe`
Implement the interface change submitted upstream as https://github.com/WebAssembly/wasi-clocks/pull/53

> We are introducing a `duration` type because it has a distinct meaning
from `instant`: an `instant` can only be compared to other `instant`s
from the exact same `monotonic-clock`, whereas a `duration` represents
a duration of time which can be compared to any other duration of time.

> The `duration` type is motivated, in part, by a desire to reuse it to
specify durations such as timeouts in other WASI proposals.

> Instead of taking a boolean specifying whether the u64 is an absolute or
relative time, `subscribe-instant` takes an `instant` type and
`subscribe-duration` takes a `duration` type.




Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 983eb0681..65b555e89 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2570,9 +2570,9 @@ dependencies = [
 
 [[package]]
 name = "target-lexicon"
-version = "0.12.3"
+version = "0.12.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1"
+checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
 
 [[package]]
 name = "tempfile"
diff --git a/Cargo.toml b/Cargo.toml
index ee34a6039..b9f539873 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -195,7 +195,7 @@ byte-array-literals = { path = "crates/wasi-preview1-component-adapter/byte-arra
 regalloc2 = "0.9.3"
 
 # cap-std family:
-target-lexicon = { version = "0.12.3", default-features = false, features = ["std"] }
+target-lexicon = { version = "0.12.12", default-features = false, features = ["std"] }
 cap-std = "2.0.0"
 cap-rand = { version = "2.0.0", features = ["small_rng"] }
 cap-fs-ext = "2.0.0"
@@ -206,7 +206,7 @@ fs-set-times = "0.20.0"
 system-interface = { version = "0.26.0", features = ["cap_std_impls"] }
 io-lifetimes = { version = "2.0.2", default-features = false }
 io-extras = "0.18.0"
-rustix = "0.38.8"
+rustix = "0.38.21"
 is-terminal = "0.4.0"
 # wit-bindgen:
 wit-bindgen = { version = "0.13.1", default-features = false }
diff --git a/cranelift/codegen/src/isa/aarch64/inst/unwind/systemv.rs b/cranelift/codegen/src/isa/aarch64/inst/unwind/systemv.rs
index a40e49e3d..efd1b0a2f 100644
--- a/cranelift/codegen/src/isa/aarch64/inst/unwind/systemv.rs
+++ b/cranelift/codegen/src/isa/aarch64/inst/unwind/systemv.rs
@@ -77,7 +77,6 @@ mod tests {
     use crate::settings::{builder, Flags};
     use crate::Context;
     use gimli::write::Address;
-    use std::str::FromStr;
     use target_lexicon::triple;
 
     #[test]
diff --git a/cranelift/codegen/src/isa/mod.rs b/cranelift/codegen/src/isa/mod.rs
index 4508c68d7..8e68e7aab 100644
--- a/cranelift/codegen/src/isa/mod.rs
+++ b/cranelift/codegen/src/isa/mod.rs
@@ -115,7 +115,6 @@ pub const ALL_ARCHITECTURES: &[&str] = &["x86_64", "aarch64", "s390x", "riscv64"
 /// Look for a supported ISA with the given `name`.
 /// Return a builder that can create a corresponding `TargetIsa`.
 pub fn lookup_by_name(name: &str) -> Result<Builder, LookupError> {
-    use alloc::str::FromStr;
     lookup(triple!(name))
 }
 
diff --git a/cranelift/codegen/src/isa/riscv64/inst/unwind/systemv.rs b/cranelift/codegen/src/isa/riscv64/inst/unwind/systemv.rs
index 75bd6cebf..4bb40f7db 100644
--- a/cranelift/codegen/src/isa/riscv64/inst/unwind/systemv.rs
+++ b/cranelift/codegen/src/isa/riscv64/inst/unwind/systemv.rs
@@ -72,7 +72,6 @@ mod tests {
     use crate::settings::{builder, Flags};
     use crate::Context;
     use gimli::write::Address;
-    use std::str::FromStr;
     use target_lexicon::triple;
 
     #[test]
diff --git a/cranelift/codegen/src/isa/s390x/inst/unwind/systemv.rs b/cranelift/codegen/src/isa/s390x/inst/unwind/systemv.rs
index 5f0a657c7..441e4134b 100644
--- a/cranelift/codegen/src/isa/s390x/inst/unwind/systemv.rs
+++ b/cranelift/codegen/src/isa/s390x/inst/unwind/systemv.rs
@@ -108,7 +108,6 @@ mod tests {
     use crate::settings::{builder, Flags};
     use crate::Context;
     use gimli::write::Address;
-    use std::str::FromStr;
     use target_lexicon::triple;
 
     #[test]
diff --git a/cranelift/codegen/src/isa/x64/inst/unwind/systemv.rs b/cranelift/codegen/src/isa/x64/inst/unwind/systemv.rs
index 68a1ecf2e..f31277cb3 100644
--- a/cranelift/codegen/src/isa/x64/inst/unwind/systemv.rs
+++ b/cranelift/codegen/src/isa/x64/inst/unwind/systemv.rs
@@ -104,7 +104,6 @@ mod tests {
     use crate::settings::{builder, Flags};
     use crate::Context;
     use gimli::write::Address;
-    use std::str::FromStr;
     use target_lexicon::triple;
 
     #[test]
diff --git a/cranelift/codegen/src/opts/bitops.isle b/cranelift/codegen/src/opts/bitops.isle
index df0029c04..018db93b6 100644
--- a/cranelift/codegen/src/opts/bitops.isle
+++ b/cranelift/codegen/src/opts/bitops.isle
@@ -129,7 +129,7 @@
 ;; (ne ty (iconst 0) v) is also canonicalized into this form via another rule
 (rule (simplify (ne cty v (iconst _ (u64_from_imm64 0))))
       (if-let c (truthy v))
-      (if-let (value_type ty) c)
+      (if-let (value_type (ty_int_ref_scalar_64 ty)) c)
       (ne cty c (iconst ty (imm64 0))))
 
 
diff --git a/crates/c-api/include/wasmtime/async.h b/crates/c-api/include/wasmtime/async.h
index 1b490003a..1ff8ccef7 100644
--- a/crates/c-api/include/wasmtime/async.h
+++ b/crates/c-api/include/wasmtime/async.h
@@ -86,7 +86,7 @@ WASMTIME_CONFIG_PROP(void, async_stack_size, uint64_t)
  * \param interval the amount of fuel at which to yield. A value of 0 will
  *        disable yielding.
  */
-WASM_API_EXTERN void
+WASM_API_EXTERN wasmtime_error_t*
 wasmtime_context_fuel_async_yield_interval(wasmtime_context_t *context,
                                            uint64_t interval);
 
diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs
index 50119b989..0facf6245 100644
--- a/crates/wasi-http/src/types.rs
+++ b/crates/wasi-http/src/types.rs
@@ -7,6 +7,7 @@ use crate::{
 };
 use anyhow::Context;
 use http_body_util::BodyExt;
+use hyper::header::HeaderName;
 use std::any::Any;
 use std::time::Duration;
 use tokio::net::TcpStream;
@@ -65,6 +66,10 @@ pub trait WasiHttpView: Send {
     {
         default_send_request(self, request)
     }
+
+    fn is_forbidden_header(&mut self, _name: &HeaderName) -> bool {
+        false
+    }
 }
 
 pub fn default_send_request(
diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index d1c8eabb6..2b21d7329 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -1,4 +1,6 @@
-use crate::bindings::http::types::{self, Error, Headers, Method, Scheme, StatusCode, Trailers};
+use crate::bindings::http::types::{
+    self, Error, HeaderError, Headers, Method, Scheme, StatusCode, Trailers,
+};
 use crate::body::{FinishMessage, HostFutureTrailers, HostFutureTrailersState};
 use crate::types::{HostIncomingRequest, HostOutgoingResponse};
 use crate::WasiHttpView;
@@ -10,6 +12,7 @@ use crate::{
     },
 };
 use anyhow::Context;
+use hyper::header::HeaderName;
 use std::any::Any;
 use wasmtime::component::Resource;
 use wasmtime_wasi::preview2::{
@@ -51,13 +54,55 @@ fn get_fields_mut<'a>(
     }
 }
 
+fn is_forbidden_header<T: WasiHttpView>(view: &mut T, name: &HeaderName) -> bool {
+    static FORBIDDEN_HEADERS: [HeaderName; 9] = [
+        hyper::header::CONNECTION,
+        HeaderName::from_static("keep-alive"),
+        hyper::header::PROXY_AUTHENTICATE,
+        hyper::header::PROXY_AUTHORIZATION,
+        HeaderName::from_static("proxy-connection"),
+        hyper::header::TE,
+        hyper::header::TRANSFER_ENCODING,
+        hyper::header::UPGRADE,
+        HeaderName::from_static("http2-settings"),
+    ];
+
+    FORBIDDEN_HEADERS.contains(name) || view.is_forbidden_header(name)
+}
+
 impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
-    fn new(&mut self, entries: Vec<(String, Vec<u8>)>) -> wasmtime::Result<Resource<HostFields>> {
+    fn new(&mut self) -> wasmtime::Result<Resource<HostFields>> {
+        let id = self
+            .table()
+            .push(HostFields::Owned {
+                fields: hyper::HeaderMap::new(),
+            })
+            .context("[new_fields] pushing fields")?;
+
+        Ok(id)
+    }
+
+    fn from_list(
+        &mut self,
+        entries: Vec<(String, Vec<u8>)>,
+    ) -> wasmtime::Result<Result<Resource<HostFields>, HeaderError>> {
         let mut map = hyper::HeaderMap::new();
 
         for (header, value) in entries {
-            let header = hyper::header::HeaderName::from_bytes(header.as_bytes())?;
-            let value = hyper::header::HeaderValue::from_bytes(&value)?;
+            let header = match hyper::header::HeaderName::from_bytes(header.as_bytes()) {
+                Ok(header) => header,
+                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            };
+
+            if is_forbidden_header(self, &header) {
+                return Ok(Err(HeaderError::Forbidden));
+            }
+
+            let value = match hyper::header::HeaderValue::from_bytes(&value) {
+                Ok(value) => value,
+                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            };
+
             map.append(header, value);
         }
 
@@ -66,7 +111,7 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
             .push(HostFields::Owned { fields: map })
             .context("[new_fields] pushing fields")?;
 
-        Ok(id)
+        Ok(Ok(id))
     }
 
     fn drop(&mut self, fields: Resource<HostFields>) -> wasmtime::Result<()> {
@@ -81,9 +126,14 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         fields: Resource<HostFields>,
         name: String,
     ) -> wasmtime::Result<Vec<Vec<u8>>> {
+        let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
+            Ok(header) => header,
+            Err(_) => return Ok(vec![]),
+        };
+
         let res = get_fields_mut(self.table(), &fields)
             .context("[fields_get] getting fields")?
-            .get_all(hyper::header::HeaderName::from_bytes(name.as_bytes())?)
+            .get_all(header)
             .into_iter()
             .map(|val| val.as_bytes().to_owned())
             .collect();
@@ -94,24 +144,42 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         &mut self,
         fields: Resource<HostFields>,
         name: String,
-        values: Vec<Vec<u8>>,
-    ) -> wasmtime::Result<()> {
-        let m = get_fields_mut(self.table(), &fields)?;
+        byte_values: Vec<Vec<u8>>,
+    ) -> wasmtime::Result<Result<(), HeaderError>> {
+        let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
+            Ok(header) => header,
+            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+        };
+
+        if is_forbidden_header(self, &header) {
+            return Ok(Err(HeaderError::Forbidden));
+        }
 
-        let header = hyper::header::HeaderName::from_bytes(name.as_bytes())?;
+        let mut values = Vec::with_capacity(byte_values.len());
+        for value in byte_values {
+            match hyper::header::HeaderValue::from_bytes(&value) {
+                Ok(value) => values.push(value),
+                Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+            }
+        }
 
+        let m =
+            get_fields_mut(self.table(), &fields).context("[fields_set] getting mutable fields")?;
         m.remove(&header);
         for value in values {
-            let value = hyper::header::HeaderValue::from_bytes(&value)?;
             m.append(&header, value);
         }
 
-        Ok(())
+        Ok(Ok(()))
     }
 
     fn delete(&mut self, fields: Resource<HostFields>, name: String) -> wasmtime::Result<()> {
+        let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
+            Ok(header) => header,
+            Err(_) => return Ok(()),
+        };
+
         let m = get_fields_mut(self.table(), &fields)?;
-        let header = hyper::header::HeaderName::from_bytes(name.as_bytes())?;
         m.remove(header);
         Ok(())
     }
@@ -121,13 +189,26 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         fields: Resource<HostFields>,
         name: String,
         value: Vec<u8>,
-    ) -> wasmtime::Result<()> {
+    ) -> wasmtime::Result<Result<(), HeaderError>> {
+        let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
+            Ok(header) => header,
+            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+        };
+
+        if is_forbidden_header(self, &header) {
+            return Ok(Err(HeaderError::Forbidden));
+        }
+
+        let value = match hyper::header::HeaderValue::from_bytes(&value) {
+            Ok(value) => value,
+            Err(_) => return Ok(Err(HeaderError::InvalidSyntax)),
+        };
+
         let m = get_fields_mut(self.table(), &fields)
             .context("[fields_append] getting mutable fields")?;
-        let header = hyper::header::HeaderName::from_bytes(name.as_bytes())?;
-        let value = hyper::header::HeaderValue::from_bytes(&value)?;
+
         m.append(header, value);
-        Ok(())
+        Ok(Ok(()))
     }
 
     fn entries(
diff --git a/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit b/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit
index a3f7fe25c..df3758b8a 100644
--- a/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit
+++ b/crates/wasi-http/wit/deps/clocks/monotonic-clock.wit
@@ -11,22 +11,34 @@
 interface monotonic-clock {
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
 
-    /// A timestamp in nanoseconds.
+    /// An instant in time, in nanoseconds. An instant is relative to an
+    /// unspecified initial value, and can only be compared to instances from
+    /// the same monotonic-clock.
     type instant = u64;
 
+    /// A duration of time, in nanoseconds.
+    type duration = u64;
+
     /// Read the current value of the clock.
     ///
     /// The clock is monotonic, therefore calling this function repeatedly will
     /// produce a sequence of non-decreasing values.
     now: func() -> instant;
 
-    /// Query the resolution of the clock.
-    resolution: func() -> instant;
+    /// Query the resolution of the clock. Returns the duration of time
+    /// corresponding to a clock tick.
+    resolution: func() -> duration;
 
-    /// Create a `pollable` which will resolve once the specified time has been
-    /// reached.
-    subscribe: func(
+    /// Create a `pollable` which will resolve once the specified instant
+    /// occured.
+    subscribe-instant: func(
         when: instant,
-        absolute: bool
+    ) -> pollable;
+
+    /// Create a `pollable` which will resolve once the given duration has
+    /// elapsed, starting at the time at which this function was called.
+    /// occured.
+    subscribe-duration: func(
+        when: duration,
     ) -> pollable;
 }
diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit
index 9cace67ab..1ab5bd117 100644
--- a/crates/wasi-http/wit/deps/http/types.wit
+++ b/crates/wasi-http/wit/deps/http/types.wit
@@ -36,6 +36,13 @@ interface types {
     unexpected-error(string)
   }
 
+  /// This tyep enumerates the different kinds of errors that may occur when
+  /// setting or appending to a `fields` resource.
+  variant header-error {
+    invalid-syntax,
+    forbidden,
+  }
+
   /// Field keys are always strings.
   type field-key = string;
 
@@ -49,6 +56,9 @@ interface types {
   /// Headers and Trailers.
   resource fields {
 
+    /// Construct an empty HTTP Fields.
+    constructor();
+
     /// Construct an HTTP Fields.
     ///
     /// The list represents each key-value pair in the Fields. Keys
@@ -59,14 +69,22 @@ interface types {
     /// Value, represented as a list of bytes. In a valid Fields, all keys
     /// and values are valid UTF-8 strings. However, values are not always
     /// well-formed, so they are represented as a raw list of bytes.
-    constructor(entries: list<tuple<field-key,field-value>>);
+    ///
+    /// An error result will be returned if any header or value was
+    /// syntactically invalid, or if a header was forbidden.
+    from-list: static func(
+      entries: list<tuple<field-key,field-value>>
+    ) -> result<fields, header-error>;
 
     /// Get all of the values corresponding to a key.
     get: func(name: field-key) -> list<field-value>;
 
     /// Set all of the values for a key. Clears any existing values for that
     /// key, if they have been set.
-    set: func(name: field-key, value: list<field-value>);
+    ///
+    /// The operation can fail if the name or value arguments are invalid, or if
+    /// the name is forbidden.
+    set: func(name: field-key, value: list<field-value>) -> result<_, header-error>;
 
     /// Delete all values for a key. Does nothing if no values for the key
     /// exist.
@@ -74,7 +92,10 @@ interface types {
 
     /// Append a value for a key. Does not change or delete any existing
     /// values for that key.
-    append: func(name: field-key, value: field-value);
+    ///
+    /// The operation can fail if the name or value arguments are invalid, or if
+    /// the name is forbidden.
+    append: func(name: field-key, value: field-value) -> result<_, header-error>;
 
 
     /// Retrieve the full set of keys and values in the Fields. Like the
diff --git a/crates/wasi-preview1-component-adapter/src/lib.rs b/crates/wasi-preview1-component-adapter/src/lib.rs
index 08978baa5..267a3ac65 100644
--- a/crates/wasi-preview1-component-adapter/src/lib.rs
+++ b/crates/wasi-preview1-component-adapter/src/lib.rs
@@ -1748,12 +1748,15 @@ pub unsafe extern "C" fn poll_oneoff(
                                 clock.timeout
                             };
 
-                            monotonic_clock::subscribe(timeout, false)
+                            monotonic_clock::subscribe_duration(timeout)
                         }
 
                         CLOCKID_MONOTONIC => {
-                            let s = monotonic_clock::subscribe(clock.timeout, absolute);
-                            s
+                            if absolute {
+                                monotonic_clock::subscribe_instant(clock.timeout)
+                            } else {
+                                monotonic_clock::subscribe_duration(clock.timeout)
+                            }
                         }
 
                         _ => return Err(ERRNO_INVAL),
diff --git a/crates/wasi/src/preview2/host/clocks.rs b/crates/wasi/src/preview2/host/clocks.rs
index 688780edb..52942f365 100644
--- a/crates/wasi/src/preview2/host/clocks.rs
+++ b/crates/wasi/src/preview2/host/clocks.rs
@@ -1,7 +1,7 @@
 #![allow(unused_variables)]
 
 use crate::preview2::bindings::{
-    clocks::monotonic_clock::{self, Instant},
+    clocks::monotonic_clock::{self, Duration as WasiDuration, Instant},
     clocks::timezone::{self, TimezoneDisplay},
     clocks::wall_clock::{self, Datetime},
 };
@@ -43,6 +43,23 @@ impl<T: WasiView> wall_clock::Host for T {
     }
 }
 
+fn subscribe_to_duration(
+    table: &mut crate::preview2::Table,
+    duration: tokio::time::Duration,
+) -> anyhow::Result<Resource<Pollable>> {
+    let sleep = 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
+        // expected by `subscribe`.
+        table.push(Deadline::Instant(deadline))?
+    } else {
+        // If the user specifies a time so far in the future we can't
+        // represent it, wait forever rather than trap.
+        table.push(Deadline::Never)?
+    };
+    subscribe(table, sleep)
+}
+
 impl<T: WasiView> monotonic_clock::Host for T {
     fn now(&mut self) -> anyhow::Result<Instant> {
         Ok(self.ctx().monotonic_clock.now())
@@ -52,30 +69,33 @@ impl<T: WasiView> monotonic_clock::Host for T {
         Ok(self.ctx().monotonic_clock.resolution())
     }
 
-    fn subscribe(&mut self, when: Instant, absolute: bool) -> anyhow::Result<Resource<Pollable>> {
+    fn subscribe_instant(&mut self, when: Instant) -> anyhow::Result<Resource<Pollable>> {
         let clock_now = self.ctx().monotonic_clock.now();
-        let duration = if absolute {
-            Duration::from_nanos(when.saturating_sub(clock_now))
+        let duration = if when > clock_now {
+            Duration::from_nanos(when - clock_now)
         } else {
-            Duration::from_nanos(when)
+            Duration::from_nanos(0)
         };
-        let deadline = tokio::time::Instant::now()
-            .checked_add(duration)
-            .ok_or_else(|| anyhow::anyhow!("time overflow: duration {duration:?}"))?;
-        // NB: this resource created here is not actually exposed to wasm, it's
-        // only an internal implementation detail used to match the signature
-        // expected by `subscribe`.
-        let sleep = self.table_mut().push(Sleep(deadline))?;
-        subscribe(self.table_mut(), sleep)
+        subscribe_to_duration(&mut self.table_mut(), duration)
+    }
+
+    fn subscribe_duration(&mut self, duration: WasiDuration) -> anyhow::Result<Resource<Pollable>> {
+        subscribe_to_duration(&mut self.table_mut(), Duration::from_nanos(duration))
     }
 }
 
-struct Sleep(tokio::time::Instant);
+enum Deadline {
+    Instant(tokio::time::Instant),
+    Never,
+}
 
 #[async_trait::async_trait]
-impl Subscribe for Sleep {
+impl Subscribe for Deadline {
     async fn ready(&mut self) {
-        tokio::time::sleep_until(self.0).await;
+        match self {
+            Deadline::Instant(instant) => tokio::time::sleep_until(*instant).await,
+            Deadline::Never => std::future::pending().await,
+        }
     }
 }
 
diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs
index a97bd1bcd..e2d1e239f 100644
--- a/crates/wasi/src/preview2/host/udp.rs
+++ b/crates/wasi/src/preview2/host/udp.rs
@@ -97,13 +97,19 @@ impl<T: WasiView> udp::HostUdpSocket for T {
             _ => return Err(ErrorCode::InvalidState.into()),
         }
 
+        // We disconnect & (re)connect in two distinct steps for two reasons:
+        // - To leave our socket instance in a consistent state in case the
+        //   connect fails.
+        // - When reconnecting to a different address, Linux sometimes fails
+        //   if there isn't a disconnect in between.
+
+        // Step #1: Disconnect
         if let UdpState::Connected = socket.udp_state {
-            // FIXME: Allow multiple (dis)connects. This needs to be supported by rustix first.
-            // rustix::net::disconnect(socket.udp_socket())?;
-            // socket.udp_state = UdpState::Bound;
-            return Err(ErrorCode::NotSupported.into());
+            disconnect(socket.udp_socket())?;
+            socket.udp_state = UdpState::Bound;
         }
 
+        // Step #2: (Re)connect
         if let Some(connect_addr) = remote_address {
             rustix::net::connect(socket.udp_socket(), &connect_addr)?;
             socket.udp_state = UdpState::Connected;
@@ -481,3 +487,12 @@ impl Subscribe for OutgoingDatagramStream {
         }
     }
 }
+
+fn disconnect<Fd: rustix::fd::AsFd>(sockfd: Fd) -> rustix::io::Result<()> {
+    match rustix::net::connect_unspec(sockfd) {
+        // BSD platforms return an error even if the socket was disconnected successfully.
+        #[cfg(target_os = "macos")]
+        Err(rustix::io::Errno::INVAL | rustix::io::Errno::AFNOSUPPORT) => Ok(()),
+        r => r,
+    }
+}
diff --git a/crates/wasi/src/preview2/preview1.rs b/crates/wasi/src/preview2/preview1.rs
index ab5a9cbd5..4b8172305 100644
--- a/crates/wasi/src/preview2/preview1.rs
+++ b/crates/wasi/src/preview2/preview1.rs
@@ -2072,9 +2072,15 @@ impl<
                         }
                         _ => return Err(types::Errno::Inval.into()),
                     };
-                    monotonic_clock::Host::subscribe(self, timeout, absolute)
-                        .context("failed to call `monotonic_clock::subscribe`")
-                        .map_err(types::Error::trap)?
+                    if absolute {
+                        monotonic_clock::Host::subscribe_instant(self, timeout)
+                            .context("failed to call `monotonic_clock::subscribe_instant`")
+                            .map_err(types::Error::trap)?
+                    } else {
+                        monotonic_clock::Host::subscribe_duration(self, timeout)
+                            .context("failed to call `monotonic_clock::subscribe_duration`")
+                            .map_err(types::Error::trap)?
+                    }
                 }
                 types::SubscriptionU::FdRead(types::SubscriptionFdReadwrite {
                     file_descriptor,
diff --git a/crates/wasi/wit/deps/clocks/monotonic-clock.wit b/crates/wasi/wit/deps/clocks/monotonic-clock.wit
index a3f7fe25c..df3758b8a 100644
--- a/crates/wasi/wit/deps/clocks/monotonic-clock.wit
+++ b/crates/wasi/wit/deps/clocks/monotonic-clock.wit
@@ -11,22 +11,34 @@
 interface monotonic-clock {
     use wasi:io/poll@0.2.0-rc-2023-11-05.{pollable};
 
-    /// A timestamp in nanoseconds.
+    /// An instant in time, in nanoseconds. An instant is relative to an
+    /// unspecified initial value, and can only be compared to instances from
+    /// the same monotonic-clock.
     type instant = u64;
 
+    /// A duration of time, in nanoseconds.
+    type duration = u64;
+
     /// Read the current value of the clock.
     ///
     /// The clock is monotonic, therefore calling this function repeatedly will
     /// produce a sequence of non-decreasing values.
     now: func() -> instant;
 
-    /// Query the resolution of the clock.
-    resolution: func() -> instant;
+    /// Query the resolution of the clock. Returns the duration of time
+    /// corresponding to a clock tick.
+    resolution: func() -> duration;
 
-    /// Create a `pollable` which will resolve once the specified time has been
-    /// reached.
-    subscribe: func(
+    /// Create a `pollable` which will resolve once the specified instant
+    /// occured.
+    subscribe-instant: func(
         when: instant,
-        absolute: bool
+    ) -> pollable;
+
+    /// Create a `pollable` which will resolve once the given duration has
+    /// elapsed, starting at the time at which this function was called.
+    /// occured.
+    subscribe-duration: func(
+        when: duration,
     ) -> pollable;
 }
diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit
index 9cace67ab..1ab5bd117 100644
--- a/crates/wasi/wit/deps/http/types.wit
+++ b/crates/wasi/wit/deps/http/types.wit
@@ -36,6 +36,13 @@ interface types {
     unexpected-error(string)
   }
 
+  /// This tyep enumerates the different kinds of errors that may occur when
+  /// setting or appending to a `fields` resource.
+  variant header-error {
+    invalid-syntax,
+    forbidden,
+  }
+
   /// Field keys are always strings.
   type field-key = string;
 
@@ -49,6 +56,9 @@ interface types {
   /// Headers and Trailers.
   resource fields {
 
+    /// Construct an empty HTTP Fields.
+    constructor();
+
     /// Construct an HTTP Fields.
     ///
     /// The list represents each key-value pair in the Fields. Keys
@@ -59,14 +69,22 @@ interface types {
     /// Value, represented as a list of bytes. In a valid Fields, all keys
     /// and values are valid UTF-8 strings. However, values are not always
     /// well-formed, so they are represented as a raw list of bytes.
-    constructor(entries: list<tuple<field-key,field-value>>);
+    ///
+    /// An error result will be returned if any header or value was
+    /// syntactically invalid, or if a header was forbidden.
+    from-list: static func(
+      entries: list<tuple<field-key,field-value>>
+    ) -> result<fields, header-error>;
 
     /// Get all of the values corresponding to a key.
     get: func(name: field-key) -> list<field-value>;
 
     /// Set all of the values for a key. Clears any existing values for that
     /// key, if they have been set.
-    set: func(name: field-key, value: list<field-value>);
+    ///
+    /// The operation can fail if the name or value arguments are invalid, or if
+    /// the name is forbidden.
+    set: func(name: field-key, value: list<field-value>) -> result<_, header-error>;
 
     /// Delete all values for a key. Does nothing if no values for the key
     /// exist.
@@ -74,7 +92,10 @@ interface types {
 
     /// Append a value for a key. Does not change or delete any existing
     /// values for that key.
-    append: func(name: field-key, value: field-value);
+    ///
+    /// The operation can fail if the name or value arguments are invalid, or if
+    /// the name is forbidden.
+    append: func(name: field-key, value: field-value) -> result<_, header-error>;
 
 
     /// Retrieve the full set of keys and values in the Fields. Like the


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/icmp.clif b/cranelift/filetests/filetests/egraph/icmp.clif
index af37fa443..46eaac465 100644
--- a/cranelift/filetests/filetests/egraph/icmp.clif
+++ b/cranelift/filetests/filetests/egraph/icmp.clif
@@ -36,3 +36,24 @@ block0(v1: i64, v2: i64):
 ;     return v4
 ; }
 
+function %icmp_simplify_does_not_build_iconst_i128() -> i8 {
+block0:
+    v0 = iconst.i64 0x6350_5050_5050_3750
+    v1 = iconcat v0, v0
+    v2 = bmask.i8 v1
+    v3 = iconst.i8 0
+    v4 = icmp ugt v2, v3
+    return v4
+}
+
+; function %icmp_simplify_does_not_build_iconst_i128() -> i8 fast {
+; block0:
+;     v0 = iconst.i64 0x6350_5050_5050_3750
+;     v1 = iconcat v0, v0  ; v0 = 0x6350_5050_5050_3750, v0 = 0x6350_5050_5050_3750
+;     v2 = bmask.i8 v1
+;     v3 = iconst.i8 0
+;     v5 = icmp ne v2, v3  ; v3 = 0
+;     v6 -> v5
+;     return v5
+; }
+
diff --git a/crates/test-programs/src/bin/api_proxy.rs b/crates/test-programs/src/bin/api_proxy.rs
index ae75c1d3b..1bcdc783d 100644
--- a/crates/test-programs/src/bin/api_proxy.rs
+++ b/crates/test-programs/src/bin/api_proxy.rs
@@ -16,7 +16,7 @@ struct T;
 
 impl bindings::exports::wasi::http::incoming_handler::Guest for T {
     fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
-        let hdrs = bindings::wasi::http::types::Headers::new(&[]);
+        let hdrs = bindings::wasi::http::types::Headers::new();
         let resp = bindings::wasi::http::types::OutgoingResponse::new(200, hdrs);
         let body = resp.body().expect("outgoing response");
 
diff --git a/crates/test-programs/src/bin/api_proxy_streaming.rs b/crates/test-programs/src/bin/api_proxy_streaming.rs
index e63223058..c2cde5c15 100644
--- a/crates/test-programs/src/bin/api_proxy_streaming.rs
+++ b/crates/test-programs/src/bin/api_proxy_streaming.rs
@@ -51,7 +51,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
 
             let response = OutgoingResponse::new(
                 200,
-                Fields::new(&[("content-type".to_string(), b"text/plain".to_vec())]),
+                Fields::from_list(&[("content-type".to_string(), b"text/plain".to_vec())]).unwrap(),
             );
 
             let mut body =
@@ -75,12 +75,13 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
         (Method::Post, Some("/echo")) => {
             let response = OutgoingResponse::new(
                 200,
-                Fields::new(
+                Fields::from_list(
                     &headers
                         .into_iter()
                         .filter_map(|(k, v)| (k == "content-type").then_some((k, v)))
                         .collect::<Vec<_>>(),
-                ),
+                )
+                .unwrap(),
             );
 
             let mut body =
@@ -108,7 +109,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
         }
 
         _ => {
-            let response = OutgoingResponse::new(405, Fields::new(&[]));
+            let response = OutgoingResponse::new(405, Fields::new());
 
             let body = response.body().expect("response should be writable");
 
@@ -137,7 +138,7 @@ async fn hash(url: &Url) -> Result<String> {
                 String::new()
             }
         )),
-        Fields::new(&[]),
+        Fields::new(),
     );
 
     let response = executor::outgoing_request_send(request).await?;
diff --git a/crates/test-programs/src/bin/http_outbound_request_invalid_header.rs b/crates/test-programs/src/bin/http_outbound_request_invalid_header.rs
new file mode 100644
index 000000000..09f173298
--- /dev/null
+++ b/crates/test-programs/src/bin/http_outbound_request_invalid_header.rs
@@ -0,0 +1,60 @@
+use test_programs::wasi::http::types::{HeaderError, Headers};
+
+fn main() {
+    let hdrs = Headers::new();
+    assert!(matches!(
+        hdrs.append(&"malformed header name".to_owned(), &b"ok value".to_vec()),
+        Err(HeaderError::InvalidSyntax)
+    ));
+
+    assert!(matches!(
+        hdrs.append(&"ok-header-name".to_owned(), &b"ok value".to_vec()),
+        Ok(())
+    ));
+
+    assert!(matches!(
+        hdrs.append(&"ok-header-name".to_owned(), &b"bad\nvalue".to_vec()),
+        Err(HeaderError::InvalidSyntax)
+    ));
+
+    assert!(matches!(
+        hdrs.append(&"Connection".to_owned(), &b"keep-alive".to_vec()),
+        Err(HeaderError::Forbidden)
+    ));
+
+    assert!(matches!(
+        hdrs.append(&"Keep-Alive".to_owned(), &b"stuff".to_vec()),
+        Err(HeaderError::Forbidden)
+    ));
+
+    assert!(matches!(
+        hdrs.append(
+            &"custom-forbidden-header".to_owned(),
+            &b"keep-alive".to_vec()
+        ),
+        Err(HeaderError::Forbidden)
+    ));
+
+    assert!(matches!(
+        hdrs.append(
+            &"Custom-Forbidden-Header".to_owned(),
+            &b"keep-alive".to_vec()
+        ),
+        Err(HeaderError::Forbidden)
+    ));
+
+    assert!(matches!(
+        Headers::from_list(&[("bad header".to_owned(), b"value".to_vec())]),
+        Err(HeaderError::InvalidSyntax)
+    ));
+
+    assert!(matches!(
+        Headers::from_list(&[("custom-forbidden-header".to_owned(), b"value".to_vec())]),
+        Err(HeaderError::Forbidden)
+    ));
+
+    assert!(matches!(
+        Headers::from_list(&[("ok-header-name".to_owned(), b"bad\nvalue".to_vec())]),
+        Err(HeaderError::InvalidSyntax)
+    ));
+}
diff --git a/crates/test-programs/src/bin/http_outbound_request_response_build.rs b/crates/test-programs/src/bin/http_outbound_request_response_build.rs
index d9a29822d..8a50ff6fa 100644
--- a/crates/test-programs/src/bin/http_outbound_request_response_build.rs
+++ b/crates/test-programs/src/bin/http_outbound_request_response_build.rs
@@ -3,10 +3,11 @@ use test_programs::wasi::http::types as http_types;
 fn main() {
     println!("Called _start");
     {
-        let headers = http_types::Headers::new(&[(
+        let headers = http_types::Headers::from_list(&[(
             "Content-Type".to_string(),
             "application/json".to_string().into_bytes(),
-        )]);
+        )])
+        .unwrap();
         let request = http_types::OutgoingRequest::new(
             &http_types::Method::Get,
             None,
@@ -21,10 +22,11 @@ fn main() {
             .unwrap();
     }
     {
-        let headers = http_types::Headers::new(&[(
+        let headers = http_types::Headers::from_list(&[(
             "Content-Type".to_string(),
             "application/text".to_string().into_bytes(),
-        )]);
+        )])
+        .unwrap();
         let response = http_types::OutgoingResponse::new(200, headers);
         let outgoing_body = response.body().unwrap();
         let response_body = outgoing_body.write().unwrap();
diff --git a/crates/test-programs/src/bin/preview1_poll_oneoff_files.rs b/crates/test-programs/src/bin/preview1_poll_oneoff_files.rs
index d1dfccaef..57778f88b 100644
--- a/crates/test-programs/src/bin/preview1_poll_oneoff_files.rs
+++ b/crates/test-programs/src/bin/preview1_poll_oneoff_files.rs
@@ -84,7 +84,7 @@ unsafe fn test_timeout() {
     );
     assert!(
         after - before >= timeout,
-        "poll_oneoff should sleep for the specified interval"
+        "poll_oneoff should sleep for the specified interval of {timeout}. before: {before}, after: {after}"
     );
 }
 
@@ -122,7 +122,7 @@ unsafe fn test_sleep() {
     );
     assert!(
         after - before >= timeout,
-        "poll_oneoff should sleep for the specified interval"
+        "poll_oneoff should sleep for the specified interval of {timeout}. before: {before}, after: {after}"
     );
 }
 
diff --git a/crates/test-programs/src/bin/preview2_ip_name_lookup.rs b/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
index 219ebba45..fc0b25de8 100644
--- a/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
+++ b/crates/test-programs/src/bin/preview2_ip_name_lookup.rs
@@ -19,7 +19,7 @@ fn main() {
     // the resolution and allows errors.
     let addresses = ip_name_lookup::resolve_addresses(&network, "github.com", None, false).unwrap();
     let lookup = addresses.subscribe();
-    let timeout = monotonic_clock::subscribe(1_000_000_000, false);
+    let timeout = monotonic_clock::subscribe_duration(1_000_000_000);
     let ready = poll::poll_list(&[&lookup, &timeout]);
     assert!(ready.len() > 0);
     match ready[0] {
diff --git a/crates/test-programs/src/bin/preview2_sleep.rs b/crates/test-programs/src/bin/preview2_sleep.rs
index a5f4d76a3..15de238dc 100644
--- a/crates/test-programs/src/bin/preview2_sleep.rs
+++ b/crates/test-programs/src/bin/preview2_sleep.rs
@@ -8,20 +8,20 @@ fn main() {
 
 fn sleep_10ms() {
     let dur = 10_000_000;
-    let p = monotonic_clock::subscribe(monotonic_clock::now() + dur, true);
+    let p = monotonic_clock::subscribe_instant(monotonic_clock::now() + dur);
     poll::poll_one(&p);
-    let p = monotonic_clock::subscribe(dur, false);
+    let p = monotonic_clock::subscribe_duration(dur);
     poll::poll_one(&p);
 }
 
 fn sleep_0ms() {
-    let p = monotonic_clock::subscribe(monotonic_clock::now(), true);
+    let p = monotonic_clock::subscribe_instant(monotonic_clock::now());
     poll::poll_one(&p);
-    let p = monotonic_clock::subscribe(0, false);
+    let p = monotonic_clock::subscribe_duration(0);
     poll::poll_one(&p);
 }
 
 fn sleep_backwards_in_time() {
-    let p = monotonic_clock::subscribe(monotonic_clock::now() - 1, true);
+    let p = monotonic_clock::subscribe_instant(monotonic_clock::now() - 1);
     poll::poll_one(&p);
 }
diff --git a/crates/test-programs/src/bin/preview2_udp_connect.rs b/crates/test-programs/src/bin/preview2_udp_connect.rs
new file mode 100644
index 000000000..5100525e8
--- /dev/null
+++ b/crates/test-programs/src/bin/preview2_udp_connect.rs
@@ -0,0 +1,41 @@
+use test_programs::wasi::sockets::network::{
+    ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network,
+};
+use test_programs::wasi::sockets::udp::UdpSocket;
+
+fn test_udp_connect_disconnect_reconnect(net: &Network, family: IpAddressFamily) {
+    let unspecified_addr = IpSocketAddress::new(IpAddress::new_unspecified(family), 0);
+    let remote1 = IpSocketAddress::new(IpAddress::new_loopback(family), 4321);
+    let remote2 = IpSocketAddress::new(IpAddress::new_loopback(family), 4320);
+
+    let client = UdpSocket::new(family).unwrap();
+    client.blocking_bind(&net, unspecified_addr).unwrap();
+
+    _ = client.stream(None).unwrap();
+    assert_eq!(client.remote_address(), Err(ErrorCode::InvalidState));
+
+    _ = client.stream(None).unwrap();
+    assert_eq!(client.remote_address(), Err(ErrorCode::InvalidState));
+
+    _ = client.stream(Some(remote1)).unwrap();
+    assert_eq!(client.remote_address(), Ok(remote1));
+
+    _ = client.stream(Some(remote1)).unwrap();
+    assert_eq!(client.remote_address(), Ok(remote1));
+
+    _ = client.stream(Some(remote2)).unwrap();
+    assert_eq!(client.remote_address(), Ok(remote2));
+
+    _ = client.stream(None).unwrap();
+    assert_eq!(client.remote_address(), Err(ErrorCode::InvalidState));
+
+    _ = client.stream(Some(remote1)).unwrap();
+    assert_eq!(client.remote_address(), Ok(remote1));
+}
+
+fn main() {
+    let net = Network::default();
+
+    test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv4);
+    test_udp_connect_disconnect_reconnect(&net, IpAddressFamily::Ipv6);
+}
diff --git a/crates/test-programs/src/http.rs b/crates/test-programs/src/http.rs
index f065b45ee..4637ec713 100644
--- a/crates/test-programs/src/http.rs
+++ b/crates/test-programs/src/http.rs
@@ -42,7 +42,7 @@ pub fn request(
     fn header_val(v: &str) -> Vec<u8> {
         v.to_string().into_bytes()
     }
-    let headers = http_types::Headers::new(
+    let headers = http_types::Headers::from_list(
         &[
             &[
                 ("User-agent".to_string(), header_val("WASI-HTTP/0.0.1")),
@@ -51,7 +51,7 @@ pub fn request(
             additional_headers.unwrap_or(&[]),
         ]
         .concat(),
-    );
+    )?;
 
     let request = http_types::OutgoingRequest::new(
         &method,
diff --git a/crates/test-programs/src/sockets.rs b/crates/test-programs/src/sockets.rs
index 3033582b6..74a65d74f 100644
--- a/crates/test-programs/src/sockets.rs
+++ b/crates/test-programs/src/sockets.rs
@@ -159,7 +159,7 @@ impl OutgoingDatagramStream {
     }
 
     pub fn blocking_send(&self, mut datagrams: &[OutgoingDatagram]) -> Result<(), ErrorCode> {
-        let timeout = monotonic_clock::subscribe(TIMEOUT_NS, false);
+        let timeout = monotonic_clock::subscribe_duration(TIMEOUT_NS);
 
         while !datagrams.is_empty() {
             let permit = self.blocking_check_send(&timeout)?;
@@ -180,7 +180,7 @@ impl OutgoingDatagramStream {
 
 impl IncomingDatagramStream {
     pub fn blocking_receive(&self, count: Range<u64>) -> Result<Vec<IncomingDatagram>, ErrorCode> {
-        let timeout = monotonic_clock::subscribe(TIMEOUT_NS, false);
+        let timeout = monotonic_clock::subscribe_duration(TIMEOUT_NS);
         let pollable = self.subscribe();
         let mut datagrams = vec![];
 
diff --git a/crates/wasi-http/tests/all/async_.rs b/crates/wasi-http/tests/all/async_.rs
index 128c1f25f..3b74482e5 100644
--- a/crates/wasi-http/tests/all/async_.rs
+++ b/crates/wasi-http/tests/all/async_.rs
@@ -49,6 +49,12 @@ async fn http_outbound_request_invalid_version() -> Result<()> {
     run(HTTP_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server).await
 }
 
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn http_outbound_request_invalid_header() -> Result<()> {
+    let server = Server::http2()?;
+    run(HTTP_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server).await
+}
+
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn http_outbound_request_unknown_method() -> Result<()> {
     let server = Server::http1()?;
diff --git a/crates/wasi-http/tests/all/main.rs b/crates/wasi-http/tests/all/main.rs
index f9c68da0a..84e3ef12d 100644
--- a/crates/wasi-http/tests/all/main.rs
+++ b/crates/wasi-http/tests/all/main.rs
@@ -72,6 +72,10 @@ impl WasiHttpView for Ctx {
             types::default_send_request(self, request)
         }
     }
+
+    fn is_forbidden_header(&mut self, name: &hyper::header::HeaderName) -> bool {
+        name.as_str() == "custom-forbidden-header"
+    }
 }
 
 fn store(engine: &Engine, server: &Server) -> Store<Ctx> {
diff --git a/crates/wasi-http/tests/all/sync.rs b/crates/wasi-http/tests/all/sync.rs
index 729b1e427..ea9328aff 100644
--- a/crates/wasi-http/tests/all/sync.rs
+++ b/crates/wasi-http/tests/all/sync.rs
@@ -48,6 +48,12 @@ fn http_outbound_request_invalid_version() -> Result<()> {
     run(HTTP_OUTBOUND_REQUEST_INVALID_VERSION_COMPONENT, &server)
 }
 
+#[test_log::test]
+fn http_outbound_request_invalid_header() -> Result<()> {
+    let server = Server::http2()?;
+    run(HTTP_OUTBOUND_REQUEST_INVALID_HEADER_COMPONENT, &server)
+}
+
 #[test_log::test]
 fn http_outbound_request_unknown_method() -> Result<()> {
     let server = Server::http1()?;
diff --git a/crates/wasi/tests/all/async_.rs b/crates/wasi/tests/all/async_.rs
index c1813476b..553e3846f 100644
--- a/crates/wasi/tests/all/async_.rs
+++ b/crates/wasi/tests/all/async_.rs
@@ -319,6 +319,10 @@ async fn preview2_tcp_bind() {
     run(PREVIEW2_TCP_BIND_COMPONENT, false).await.unwrap()
 }
 #[test_log::test(tokio::test(flavor = "multi_thread"))]
+async fn preview2_udp_connect() {
+    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).await.unwrap()
+}
+#[test_log::test(tokio::test(flavor = "multi_thread"))]
 async fn preview2_udp_sample_application() {
     run(PREVIEW2_UDP_SAMPLE_APPLICATION_COMPONENT, false)
         .await
diff --git a/crates/wasi/tests/all/sync.rs b/crates/wasi/tests/all/sync.rs
index 4347e01d9..0e087ab8c 100644
--- a/crates/wasi/tests/all/sync.rs
+++ b/crates/wasi/tests/all/sync.rs
@@ -262,6 +262,10 @@ fn preview2_tcp_bind() {
     run(PREVIEW2_TCP_BIND_COMPONENT, false).unwrap()
 }
 #[test_log::test]
+fn preview2_udp_connect() {
+    run(PREVIEW2_UDP_CONNECT_COMPONENT, false).unwrap()
+}
+#[test_log::test]
 fn preview2_udp_sample_application() {
     run(PREVIEW2_UDP_SAMPLE_APPLICATION_COMPONENT, false).unwrap()
 }