Repository: bytecodealliance/wasmtime
Pull request #7411 (2023-10-30 19:56:56)

wasi-sockets: (UDP) Allow reconnects & disconnects
Resolves FIXME comment introduced in https://github.com/bytecodealliance/wasmtime/pull/7243


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index ee34a6039..b9e0f1a92 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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/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/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/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/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/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
diff --git a/docs/lang-bash.md b/docs/lang-bash.md
index fba5f9a74..bc1f8f829 100644
--- a/docs/lang-bash.md
+++ b/docs/lang-bash.md
@@ -30,7 +30,7 @@ function gcd() {
   local x=$(($1))
   local y=$(($2))
   # Invoke GCD from module; suppress stderr
-  local result=$(wasmtime examples/gcd.wat --invoke gcd $x $y 2>/dev/null)
+  local result=$(wasmtime --invoke gcd examples/gcd.wat $x $y 2>/dev/null)
   echo "$result"
 }
 
diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs
index 3ca25c7df..a8e905505 100644
--- a/src/bin/wasmtime.rs
+++ b/src/bin/wasmtime.rs
@@ -24,7 +24,7 @@ use clap::Parser;
                   \n\
                   Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\
                   \n  \
-                  wasmtime example.wasm --invoke add 1 2\n",
+                  wasmtime --invoke add example.wasm 1 2\n",
 
     // This option enables the pattern below where we ask clap to parse twice
     // sorta: once where it's trying to find a subcommand and once assuming


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/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/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()
 }