diff --git a/Cargo.lock b/Cargo.lock index be667f8883d8..3ceb063cb122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5186,9 +5186,11 @@ dependencies = [ "bytes", "futures", "rustls", + "test-log", "test-programs-artifacts", "tokio", "tokio-rustls", + "tracing", "wasmtime", "wasmtime-wasi", "webpki-roots", diff --git a/Cargo.toml b/Cargo.toml index 30c846c9a860..ca6366abd569 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -520,6 +520,7 @@ component-model-async = [ "component-model", "wasmtime-wasi?/p3", "wasmtime-wasi-http?/p3", + "wasmtime-wasi-tls?/p3", "dep:futures", ] rr = ["wasmtime/rr", "component-model", "wasmtime-cli-flags/rr", "run"] diff --git a/ci/vendor-wit.sh b/ci/vendor-wit.sh index 150b4ee149bf..1b3e6c7f99e5 100755 --- a/ci/vendor-wit.sh +++ b/ci/vendor-wit.sh @@ -16,21 +16,22 @@ get_github() { local repo=$1 local tag=$2 local path=$3 + local prefix=${4:-wit} rm -rf "$path" mkdir -p "$path" - cached_extracted_dir="$cache_dir/$repo-$tag" + cached_extracted_dir="$cache_dir/$prefix/$repo-$tag" if [[ ! -d $cached_extracted_dir ]]; then mkdir -p $cached_extracted_dir curl --retry 5 --retry-all-errors -sLO https://github.com/WebAssembly/$repo/archive/$tag.tar.gz tar xzf $tag.tar.gz --strip-components=1 -C $cached_extracted_dir rm $tag.tar.gz - rm -rf $cached_extracted_dir/wit/deps* + rm -rf $cached_extracted_dir/${prefix}/deps* fi - cp -r $cached_extracted_dir/wit/* $path + cp -r $cached_extracted_dir/${prefix}/* $path } p2=0.2.6 @@ -65,6 +66,8 @@ mkdir -p crates/wasi-tls/wit/deps wkg get --format wit --overwrite "wasi:io@$p2" -o "crates/wasi-tls/wit/deps/io.wit" get_github wasi-tls v0.2.0-draft+505fc98 crates/wasi-tls/wit/deps/tls +get_github wasi-tls v0.2.0-draft+6781ae2 crates/wasi-tls/src/p3/wit/deps/tls wit-0.3.0-draft + rm -rf crates/wasi-config/wit/deps mkdir -p crates/wasi-config/wit/deps get_github wasi-config v0.2.0-rc.1 crates/wasi-config/wit/deps/config diff --git a/crates/test-programs/artifacts/build.rs b/crates/test-programs/artifacts/build.rs index a9d81098b2b9..d6f122e4beeb 100644 --- a/crates/test-programs/artifacts/build.rs +++ b/crates/test-programs/artifacts/build.rs @@ -79,19 +79,20 @@ impl Artifacts { // generates a `foreach_*` macro below. let kind = match test.name.as_str() { s if s.starts_with("p1_") => "p1", - s if s.starts_with("p2_http_") => "p2_http", - s if s.starts_with("p2_cli_") => "p2_cli", s if s.starts_with("p2_api_") => "p2_api", + s if s.starts_with("p2_cli_") => "p2_cli", + s if s.starts_with("p2_http_") => "p2_http", + s if s.starts_with("p2_tls_") => "p2_tls", s if s.starts_with("p2_") => "p2", s if s.starts_with("nn_") => "nn", s if s.starts_with("piped_") => "piped", s if s.starts_with("dwarf_") => "dwarf", s if s.starts_with("config_") => "config", s if s.starts_with("keyvalue_") => "keyvalue", - s if s.starts_with("tls_") => "tls", s if s.starts_with("async_") => "async", - s if s.starts_with("p3_http_") => "p3_http", s if s.starts_with("p3_api_") => "p3_api", + s if s.starts_with("p3_http_") => "p3_http", + s if s.starts_with("p3_tls_") => "p3_tls", s if s.starts_with("p3_") => "p3", s if s.starts_with("fuzz_") => "fuzz", // If you're reading this because you hit this panic, either add diff --git a/crates/test-programs/src/bin/tls_sample_application.rs b/crates/test-programs/src/bin/p2_tls_sample_application.rs similarity index 100% rename from crates/test-programs/src/bin/tls_sample_application.rs rename to crates/test-programs/src/bin/p2_tls_sample_application.rs diff --git a/crates/test-programs/src/bin/p3_tls_sample_application.rs b/crates/test-programs/src/bin/p3_tls_sample_application.rs new file mode 100644 index 000000000000..251c43553374 --- /dev/null +++ b/crates/test-programs/src/bin/p3_tls_sample_application.rs @@ -0,0 +1,177 @@ +use anyhow::{Context as _, Result, anyhow}; +use core::future::Future; +use futures::try_join; +use test_programs::p3::wasi::sockets::ip_name_lookup::resolve_addresses; +use test_programs::p3::wasi::sockets::types::{IpAddress, IpSocketAddress, TcpSocket}; +use test_programs::p3::wasi::tls::client::Connector; +use test_programs::p3::wit_stream; + +struct Component; + +test_programs::p3::export!(Component); + +const PORT: u16 = 443; + +async fn test_tls_sample_application(domain: &str, ip: IpAddress) -> Result<()> { + let request = format!( + "GET / HTTP/1.1\r\nHost: {domain}\r\nUser-Agent: wasmtime-wasi-rust\r\nConnection: close\r\n\r\n" + ); + + let sock = TcpSocket::create(ip.family()).unwrap(); + sock.connect(IpSocketAddress::new(ip, PORT)) + .await + .context("tcp connect failed")?; + + let conn = Connector::new(); + + let (sock_rx, sock_rx_fut) = sock.receive(); + let (tls_rx, tls_rx_fut) = conn.receive(sock_rx); + + let (mut data_tx, data_rx) = wit_stream::new(); + let (tls_tx, tls_tx_err_fut) = conn.send(data_rx); + let sock_tx_fut = sock.send(tls_tx); + + try_join!( + async { + Connector::connect(conn, domain.into()) + .await + .map_err(|err| { + anyhow!(err.to_debug_string()).context("failed to establish connection") + }) + }, + async { + let buf = data_tx.write_all(request.into()).await; + assert!(buf.is_empty()); + drop(data_tx); + Ok(()) + }, + async { + let response = tls_rx.collect().await; + let response = String::from_utf8(response)?; + if response.contains("HTTP/1.1 200 OK") { + Ok(()) + } else { + Err(anyhow!("server did not respond with 200 OK: {response}")) + } + }, + async { sock_rx_fut.await.context("failed to receive ciphertext") }, + async { sock_tx_fut.await.context("failed to send ciphertext") }, + async { + tls_rx_fut + .await + .map_err(|err| anyhow!(err.to_debug_string())) + .context("failed to receive plaintext") + }, + async { + tls_tx_err_fut + .await + .map_err(|err| anyhow!(err.to_debug_string())) + .context("failed to send plaintext") + }, + )?; + Ok(()) +} + +/// This test sets up a TCP connection using one domain, and then attempts to +/// perform a TLS handshake using another unrelated domain. This should result +/// in a handshake error. +async fn test_tls_invalid_certificate(_domain: &str, ip: IpAddress) -> Result<()> { + const BAD_DOMAIN: &str = "wrongdomain.localhost"; + + let sock = TcpSocket::create(ip.family()).unwrap(); + sock.connect(IpSocketAddress::new(ip, PORT)) + .await + .context("tcp connect failed")?; + + let conn = Connector::new(); + + let (sock_rx, sock_rx_fut) = sock.receive(); + let (tls_rx, tls_rx_fut) = conn.receive(sock_rx); + + let (_, data_rx) = wit_stream::new(); + let (tls_tx, tls_tx_err_fut) = conn.send(data_rx); + let sock_tx_fut = sock.send(tls_tx); + let res = try_join!( + async { + Connector::connect(conn, BAD_DOMAIN.into()) + .await + .expect("`connect` failed"); + Ok(()) + }, + async { + let response = tls_rx.collect().await; + assert_eq!(response, []); + Ok(()) + }, + async { + sock_rx_fut.await.expect("failed to receive ciphertext"); + Ok(()) + }, + async { + sock_tx_fut.await.expect("failed to send ciphertext"); + Ok(()) + }, + async { tls_rx_fut.await }, + async { tls_tx_err_fut.await }, + ); + match res { + Err(e) => { + let debug_string = e.to_debug_string(); + // We're expecting an error regarding certificates in some form or + // another. When we add more TLS backends this naive check will + // likely need to be revisited/expanded: + if debug_string.contains("certificate") || debug_string.contains("HandshakeFailure") { + return Ok(()); + } + Err(anyhow!(debug_string)) + } + Ok(_) => panic!("expecting server name mismatch"), + } +} + +async fn try_live_endpoints<'a, Fut>(test: impl Fn(&'a str, IpAddress) -> Fut) +where + Fut: Future> + 'a, +{ + // since this is testing remote endpoints to ensure system cert store works + // the test uses a couple different endpoints to reduce the number of flakes + const DOMAINS: &[&str] = &[ + "example.com", + "api.github.com", + "docs.wasmtime.dev", + "bytecodealliance.org", + "www.rust-lang.org", + ]; + + for &domain in DOMAINS { + let result = (|| async { + let ip = resolve_addresses(domain.into()) + .await? + .first() + .map(|a| a.to_owned()) + .ok_or_else(|| anyhow!("DNS lookup failed."))?; + test(domain, ip).await + })(); + + match result.await { + Ok(()) => return, + Err(e) => { + eprintln!("test for {domain} failed: {e:#}"); + } + } + } + + panic!("all tests failed"); +} + +impl test_programs::p3::exports::wasi::cli::run::Guest for Component { + async fn run() -> Result<(), ()> { + println!("sample app"); + try_live_endpoints(test_tls_sample_application).await; + println!("invalid cert"); + try_live_endpoints(test_tls_invalid_certificate).await; + Ok(()) + } +} + +fn main() {} diff --git a/crates/test-programs/src/p3/mod.rs b/crates/test-programs/src/p3/mod.rs index d5dea4f18439..7d2eb951ed71 100644 --- a/crates/test-programs/src/p3/mod.rs +++ b/crates/test-programs/src/p3/mod.rs @@ -7,6 +7,7 @@ wit_bindgen::generate!({ world testp3 { include wasi:cli/imports@0.3.0-rc-2026-02-09; + include wasi:tls/imports@0.3.0-draft; import wasi:http/types@0.3.0-rc-2026-02-09; import wasi:http/client@0.3.0-rc-2026-02-09; import wasi:http/handler@0.3.0-rc-2026-02-09; @@ -14,7 +15,10 @@ wit_bindgen::generate!({ export wasi:cli/run@0.3.0-rc-2026-02-09; } ", - path: "../wasi-http/src/p3/wit", + path: [ + "../wasi-http/src/p3/wit", + "../wasi-tls/src/p3/wit", + ], world: "wasmtime:test/testp3", default_bindings_module: "test_programs::p3", pub_export_macro: true, diff --git a/crates/wasi-tls-nativetls/tests/main.rs b/crates/wasi-tls-nativetls/tests/main.rs index 0aa344a815ec..be4ab471caba 100644 --- a/crates/wasi-tls-nativetls/tests/main.rs +++ b/crates/wasi-tls-nativetls/tests/main.rs @@ -60,9 +60,9 @@ macro_rules! assert_test_exists { }; } -test_programs_artifacts::foreach_tls!(assert_test_exists); +test_programs_artifacts::foreach_p2_tls!(assert_test_exists); #[tokio::test(flavor = "multi_thread")] -async fn tls_sample_application() -> Result<()> { - run_test(test_programs_artifacts::TLS_SAMPLE_APPLICATION_COMPONENT).await +async fn p2_tls_sample_application() -> Result<()> { + run_test(test_programs_artifacts::P2_TLS_SAMPLE_APPLICATION_COMPONENT).await } diff --git a/crates/wasi-tls-openssl/tests/main.rs b/crates/wasi-tls-openssl/tests/main.rs index d09c0939aacb..a1683d549017 100644 --- a/crates/wasi-tls-openssl/tests/main.rs +++ b/crates/wasi-tls-openssl/tests/main.rs @@ -60,9 +60,9 @@ macro_rules! assert_test_exists { }; } -test_programs_artifacts::foreach_tls!(assert_test_exists); +test_programs_artifacts::foreach_p2_tls!(assert_test_exists); #[tokio::test(flavor = "multi_thread")] -async fn tls_sample_application() -> Result<()> { - run_test(test_programs_artifacts::TLS_SAMPLE_APPLICATION_COMPONENT).await +async fn p2_tls_sample_application() -> Result<()> { + run_test(test_programs_artifacts::P2_TLS_SAMPLE_APPLICATION_COMPONENT).await } diff --git a/crates/wasi-tls/Cargo.toml b/crates/wasi-tls/Cargo.toml index a94af76ca3c5..ab277ebd6c70 100644 --- a/crates/wasi-tls/Cargo.toml +++ b/crates/wasi-tls/Cargo.toml @@ -11,6 +11,10 @@ description = "Wasmtime implementation of the wasi-tls API" [lints] workspace = true +[features] +default = [] +p3 = ["wasmtime-wasi/p3", "wasmtime/component-model-async"] + [dependencies] bytes = { workspace = true } tokio = { workspace = true, features = [ @@ -19,6 +23,7 @@ tokio = { workspace = true, features = [ "time", "io-util", ] } +tracing = { workspace = true } wasmtime = { workspace = true, features = ["runtime", "component-model"] } wasmtime-wasi = { workspace = true } @@ -31,3 +36,4 @@ test-programs-artifacts = { workspace = true } wasmtime-wasi = { workspace = true } tokio = { workspace = true, features = ["macros"] } futures = { workspace = true } +test-log = { workspace = true } diff --git a/crates/wasi-tls/src/lib.rs b/crates/wasi-tls/src/lib.rs index 0f011f0bde29..cc14f379bef5 100644 --- a/crates/wasi-tls/src/lib.rs +++ b/crates/wasi-tls/src/lib.rs @@ -74,6 +74,8 @@ use wasmtime::component::{HasData, ResourceTable}; pub mod bindings; mod host; mod io; +#[cfg(feature = "p3")] +pub mod p3; mod rustls; pub use bindings::types::LinkOptions; diff --git a/crates/wasi-tls/src/p3/bindings.rs b/crates/wasi-tls/src/p3/bindings.rs new file mode 100644 index 000000000000..5958aebe47b8 --- /dev/null +++ b/crates/wasi-tls/src/p3/bindings.rs @@ -0,0 +1,20 @@ +//! Raw bindings to the `wasi:tls` package. + +#[expect(missing_docs, reason = "generated code")] +mod generated { + wasmtime::component::bindgen!({ + path: "src/p3/wit", + world: "wasi:tls/imports", + imports: { + "wasi:tls/client.[method]connector.receive": trappable | tracing | store, + "wasi:tls/client.[method]connector.send": trappable | tracing | store, + default: trappable | tracing + }, + with: { + "wasi:tls/client.connector": crate::p3::Connector, + "wasi:tls/types.error": String, + }, + }); +} + +pub use self::generated::wasi::*; diff --git a/crates/wasi-tls/src/p3/host/client.rs b/crates/wasi-tls/src/p3/host/client.rs new file mode 100644 index 000000000000..6ce4a6ecbf14 --- /dev/null +++ b/crates/wasi-tls/src/p3/host/client.rs @@ -0,0 +1,158 @@ +use super::{ + CiphertextConsumer, CiphertextProducer, Pending, PlaintextConsumer, PlaintextProducer, + mk_delete, mk_get_mut, mk_push, push_error, +}; +use crate::p3::bindings::tls::client::{Connector, Host, HostConnector, HostConnectorWithStore}; +use crate::p3::bindings::tls::types::Error; +use crate::p3::host::ResultProducer; +use crate::p3::{TlsStream, WasiTls, WasiTlsCtxView}; +use std::sync::{Arc, Mutex}; +use tokio::sync::oneshot; +use wasmtime::component::{Access, Accessor, FutureReader, Resource, StreamReader}; + +mk_push!(Connector, push_connector, "client connector"); +mk_get_mut!(Connector, get_connector_mut, "client connector"); +mk_delete!(Connector, delete_connector, "client connector"); + +impl Host for WasiTlsCtxView<'_> {} + +impl HostConnector for WasiTlsCtxView<'_> { + fn new(&mut self) -> wasmtime::Result> { + push_connector(&mut self.table, Connector::default()) + } + + fn drop(&mut self, conn: Resource) -> wasmtime::Result<()> { + delete_connector(&mut self.table, conn)?; + Ok(()) + } +} + +impl HostConnectorWithStore for WasiTls { + fn send( + mut store: Access, + conn: Resource, + cleartext: StreamReader, + ) -> wasmtime::Result<(StreamReader, FutureReader>>)> + where + T: 'static, + { + let conn @ Connector { send_tx: None, .. } = get_connector_mut(store.get().table, &conn)? + else { + return Err(wasmtime::Error::msg("`send` was already called")); + }; + + let (cons_tx, cons_rx) = oneshot::channel(); + let (prod_tx, prod_rx) = oneshot::channel(); + let (err_tx, err_rx) = oneshot::channel(); + + conn.send_tx = Some((prod_tx, cons_tx, err_tx)); + + let rx = StreamReader::new(&mut store, Pending::from(prod_rx)); + cleartext.pipe(&mut store, Pending::from(cons_rx)); + let getter = store.getter(); + Ok(( + rx, + FutureReader::new(store, ResultProducer { rx: err_rx, getter }), + )) + } + + fn receive( + mut store: Access, + conn: Resource, + ciphertext: StreamReader, + ) -> wasmtime::Result<(StreamReader, FutureReader>>)> + where + T: 'static, + { + let conn @ Connector { + receive_tx: None, .. + } = get_connector_mut(store.get().table, &conn)? + else { + return Err(wasmtime::Error::msg("`receive` was already called")); + }; + + let (cons_tx, cons_rx) = oneshot::channel(); + let (prod_tx, prod_rx) = oneshot::channel(); + let (err_tx, err_rx) = oneshot::channel(); + + conn.receive_tx = Some((prod_tx, cons_tx, err_tx)); + + let rx = StreamReader::new(&mut store, Pending::from(prod_rx)); + ciphertext.pipe(&mut store, Pending::from(cons_rx)); + let getter = store.getter(); + Ok(( + rx, + FutureReader::new(store, ResultProducer { rx: err_rx, getter }), + )) + } + + async fn connect( + store: &Accessor, + conn: Resource, + server_name: String, + ) -> wasmtime::Result>> + where + T: 'static, + { + store.with(|mut store| { + let server_name = match server_name.try_into() { + Ok(name) => name, + Err(err) => { + let err = push_error(store.get().table, format!("{err}"))?; + return Ok(Err(err)); + } + }; + + let Connector { + receive_tx: Some((receive_prod_tx, receive_cons_tx, receive_err_tx)), + send_tx: Some((send_prod_tx, send_cons_tx, send_err_tx)), + } = delete_connector(store.get().table, conn)? + else { + let err = push_error( + store.get().table, + format!("`send` and `receive` must be called prior to calling `connect`"), + )?; + return Ok(Err(err)); + }; + + let roots = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.into(), + }; + let config = rustls::ClientConfig::builder() + .with_root_certificates(roots) + .with_no_client_auth(); + + let conn = match rustls::ClientConnection::new(Arc::from(config), server_name) { + Ok(conn) => conn, + Err(err) => { + let err = push_error(store.get().table, format!("{err}"))?; + return Ok(Err(err)); + } + }; + + let stream = Arc::new(Mutex::new(TlsStream::new(conn))); + + let send_err_tx = Arc::new(Mutex::new(Some(send_err_tx))); + let _ = send_cons_tx.send(PlaintextConsumer { + stream: Arc::clone(&stream), + error_tx: Arc::clone(&send_err_tx), + }); + let _ = send_prod_tx.send(CiphertextProducer { + stream: Arc::clone(&stream), + error_tx: send_err_tx, + }); + + let receive_err_tx = Arc::new(Mutex::new(Some(receive_err_tx))); + let _ = receive_cons_tx.send(CiphertextConsumer { + stream: Arc::clone(&stream), + error_tx: Arc::clone(&receive_err_tx), + }); + let _ = receive_prod_tx.send(PlaintextProducer { + stream, + error_tx: receive_err_tx, + }); + + Ok(Ok(())) + }) + } +} diff --git a/crates/wasi-tls/src/p3/host/mod.rs b/crates/wasi-tls/src/p3/host/mod.rs new file mode 100644 index 000000000000..32d656b227a4 --- /dev/null +++ b/crates/wasi-tls/src/p3/host/mod.rs @@ -0,0 +1,497 @@ +use crate::p3::bindings::tls::client::Error; +use crate::p3::{TlsStream, TlsStreamArc, WasiTlsCtxView}; +use core::ops::DerefMut; +use core::pin::Pin; +use core::task::{Context, Poll, Waker}; +use std::io::{Read as _, Write as _}; +use std::sync::{Arc, Mutex}; +use tokio::sync::oneshot; +use wasmtime::StoreContextMut; +use wasmtime::component::{ + Destination, FutureProducer, Resource, Source, StreamConsumer, StreamProducer, StreamResult, +}; + +mod client; +mod types; + +macro_rules! mk_push { + ($t:ty, $f:ident, $desc:literal) => { + #[track_caller] + #[inline] + pub fn $f( + table: &mut wasmtime::component::ResourceTable, + value: $t, + ) -> wasmtime::Result> { + use wasmtime::error::Context as _; + + table + .push(value) + .context(concat!("failed to push ", $desc, " resource to table")) + } + }; +} + +macro_rules! mk_get { + ($t:ty, $f:ident, $desc:literal) => { + #[track_caller] + #[inline] + pub fn $f<'a>( + table: &'a wasmtime::component::ResourceTable, + resource: &'a wasmtime::component::Resource<$t>, + ) -> wasmtime::Result<&'a $t> { + use wasmtime::error::Context as _; + + table + .get(resource) + .context(concat!("failed to get ", $desc, " resource from table")) + } + }; +} + +macro_rules! mk_get_mut { + ($t:ty, $f:ident, $desc:literal) => { + #[track_caller] + #[inline] + pub fn $f<'a>( + table: &'a mut wasmtime::component::ResourceTable, + resource: &'a wasmtime::component::Resource<$t>, + ) -> wasmtime::Result<&'a mut $t> { + use wasmtime::error::Context as _; + + table.get_mut(resource).context(concat!( + "failed to get ", + $desc, + " resource from table" + )) + } + }; +} + +macro_rules! mk_delete { + ($t:ty, $f:ident, $desc:literal) => { + #[track_caller] + #[inline] + pub fn $f( + table: &mut wasmtime::component::ResourceTable, + resource: wasmtime::component::Resource<$t>, + ) -> wasmtime::Result<$t> { + use wasmtime::error::Context as _; + + table.delete(resource).context(concat!( + "failed to delete ", + $desc, + " resource from table" + )) + } + }; +} + +pub(crate) use {mk_delete, mk_get, mk_get_mut, mk_push}; + +mk_push!(Error, push_error, "error"); + +struct Pending { + inner_rx: oneshot::Receiver, + inner: Option, +} + +impl From> for Pending { + fn from(rx: oneshot::Receiver) -> Self { + Self { + inner_rx: rx, + inner: None, + } + } +} + +impl StreamProducer for Pending +where + T: StreamProducer + Unpin, +{ + type Item = >::Item; + type Buffer = >::Buffer; + + fn poll_produce<'a>( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut<'a, D>, + dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + if let Some(ref mut inner) = self.inner { + return Pin::new(inner).poll_produce(cx, store, dst, finish); + } + match Pin::new(&mut self.inner_rx).poll(cx) { + Poll::Ready(Ok(inner)) => { + self.inner = Some(inner); + return self.poll_produce(cx, store, dst, finish); + } + Poll::Ready(Err(..)) => Poll::Ready(Ok(StreamResult::Dropped)), + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +impl StreamConsumer for Pending +where + T: StreamConsumer + Unpin, +{ + type Item = >::Item; + + fn poll_consume( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + finish: bool, + ) -> Poll> { + if let Some(ref mut inner) = self.inner { + return Pin::new(inner).poll_consume(cx, store, src, finish); + } + match Pin::new(&mut self.inner_rx).poll(cx) { + Poll::Ready(Ok(inner)) => { + self.inner = Some(inner); + return self.poll_consume(cx, store, src, finish); + } + Poll::Ready(Err(..)) => Poll::Ready(Ok(StreamResult::Dropped)), + Poll::Pending if finish => Poll::Ready(Ok(StreamResult::Cancelled)), + Poll::Pending => Poll::Pending, + } + } +} + +pub struct CiphertextConsumer { + stream: TlsStreamArc, + error_tx: Arc>>>, +} + +impl Drop for CiphertextConsumer { + fn drop(&mut self) { + let mut stream = self.stream.lock(); + let TlsStream { + ciphertext_consumer_dropped, + plaintext_producer, + ciphertext_producer, + .. + } = stream.as_deref_mut().unwrap(); + *ciphertext_consumer_dropped = true; + plaintext_producer.take().map(Waker::wake); + ciphertext_producer.take().map(Waker::wake); + } +} + +impl StreamConsumer for CiphertextConsumer +where + T: DerefMut> + Send + 'static, +{ + type Item = u8; + + fn poll_consume( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + finish: bool, + ) -> Poll> { + let mut error_tx = self.error_tx.lock().unwrap(); + if error_tx.is_none() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + + let mut stream = self.stream.lock(); + let TlsStream { + conn, + ciphertext_consumer, + ciphertext_producer, + plaintext_consumer, + plaintext_producer, + .. + } = stream.as_deref_mut().unwrap(); + + if !conn.wants_read() { + if finish { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + *ciphertext_consumer = Some(cx.waker().clone()); + return Poll::Pending; + } + + let mut src = src.as_direct(store); + if src.remaining().is_empty() { + return Poll::Ready(Ok(StreamResult::Completed)); + } + let n = conn.read_tls(&mut src)?; + debug_assert_ne!(n, 0); + + let state = match conn.process_new_packets() { + Ok(state) => state, + Err(err) => { + _ = error_tx.take().unwrap().send(err); + ciphertext_producer.take().map(Waker::wake); + plaintext_consumer.take().map(Waker::wake); + plaintext_producer.take().map(Waker::wake); + return Poll::Ready(Ok(StreamResult::Dropped)); + } + }; + + if state.plaintext_bytes_to_read() > 0 { + plaintext_producer.take().map(Waker::wake); + } + + if state.tls_bytes_to_write() > 0 { + ciphertext_producer.take().map(Waker::wake); + } + + if state.peer_has_closed() { + Poll::Ready(Ok(StreamResult::Dropped)) + } else { + Poll::Ready(Ok(StreamResult::Completed)) + } + } +} + +pub struct PlaintextProducer { + stream: TlsStreamArc, + error_tx: Arc>>>, +} + +impl StreamProducer for PlaintextProducer +where + T: DerefMut> + Send + 'static, +{ + type Item = u8; + type Buffer = Option; // unused + + fn poll_produce<'a>( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut<'a, D>, + dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + let mut error_tx = self.error_tx.lock().unwrap(); + if error_tx.is_none() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + + let mut stream = self.stream.lock(); + let TlsStream { + conn, + ciphertext_consumer_dropped, + ciphertext_consumer, + ciphertext_producer, + plaintext_consumer, + plaintext_producer, + .. + } = stream.as_deref_mut().unwrap(); + + let state = match conn.process_new_packets() { + Ok(state) => state, + Err(err) => { + _ = error_tx.take().unwrap().send(err); + ciphertext_consumer.take().map(Waker::wake); + ciphertext_producer.take().map(Waker::wake); + plaintext_consumer.take().map(Waker::wake); + return Poll::Ready(Ok(StreamResult::Dropped)); + } + }; + + if state.plaintext_bytes_to_read() == 0 { + if state.peer_has_closed() || *ciphertext_consumer_dropped { + return Poll::Ready(Ok(StreamResult::Dropped)); + } else if finish { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + *plaintext_producer = Some(cx.waker().clone()); + return Poll::Pending; + } + + let mut dst = dst.as_direct(store, state.plaintext_bytes_to_read()); + let buf = dst.remaining(); + if buf.is_empty() { + return Poll::Ready(Ok(StreamResult::Completed)); + } + let n = conn.reader().read(buf)?; + debug_assert_ne!(n, 0); + dst.mark_written(n); + if conn.wants_read() { + ciphertext_consumer.take().map(Waker::wake); + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +pub struct PlaintextConsumer +where + T: DerefMut> + Send + 'static, +{ + stream: TlsStreamArc, + error_tx: Arc>>>, +} + +impl Drop for PlaintextConsumer +where + T: DerefMut> + Send + 'static, +{ + fn drop(&mut self) { + let mut stream = self.stream.lock(); + let TlsStream { + plaintext_consumer_dropped, + ciphertext_producer, + .. + } = stream.as_deref_mut().unwrap(); + *plaintext_consumer_dropped = true; + ciphertext_producer.take().map(Waker::wake); + } +} + +impl StreamConsumer for PlaintextConsumer +where + T: DerefMut> + Send + 'static, + U: 'static, +{ + type Item = u8; + + fn poll_consume( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut, + src: Source, + finish: bool, + ) -> Poll> { + let error_tx = self.error_tx.lock().unwrap(); + if error_tx.is_none() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + + let mut stream = self.stream.lock(); + let TlsStream { + conn, + ciphertext_producer, + plaintext_consumer, + .. + } = stream.as_deref_mut().unwrap(); + + let mut src = src.as_direct(store); + if src.remaining().is_empty() { + return Poll::Ready(Ok(StreamResult::Completed)); + } + + let mut dst = conn.writer(); + let n = dst.write(src.remaining())?; + if n == 0 { + if finish { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + *plaintext_consumer = Some(cx.waker().clone()); + return Poll::Pending; + } + src.mark_read(n); + dst.flush()?; + if conn.wants_write() { + ciphertext_producer.take().map(Waker::wake); + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +pub struct CiphertextProducer { + stream: TlsStreamArc, + error_tx: Arc>>>, +} + +impl StreamProducer for CiphertextProducer +where + T: DerefMut> + Send + 'static, +{ + type Item = u8; + type Buffer = Option; // unused + + fn poll_produce<'a>( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + store: StoreContextMut<'a, D>, + dst: Destination<'a, Self::Item, Self::Buffer>, + finish: bool, + ) -> Poll> { + let mut error_tx = self.error_tx.lock().unwrap(); + if error_tx.is_none() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + + let mut stream = self.stream.lock(); + let TlsStream { + conn, + plaintext_consumer_dropped, + ciphertext_consumer_dropped, + ciphertext_consumer, + ciphertext_producer, + plaintext_consumer, + plaintext_producer, + } = stream.as_deref_mut().unwrap(); + + if !conn.wants_write() { + if *plaintext_consumer_dropped && *ciphertext_consumer_dropped { + return Poll::Ready(Ok(StreamResult::Dropped)); + } else if finish { + return Poll::Ready(Ok(StreamResult::Cancelled)); + } + *ciphertext_producer = Some(cx.waker().clone()); + plaintext_consumer.take().map(Waker::wake); + return Poll::Pending; + } + + let state = match conn.process_new_packets() { + Ok(state) => state, + Err(err) => { + _ = error_tx.take().unwrap().send(err); + ciphertext_consumer.take().map(Waker::wake); + plaintext_consumer.take().map(Waker::wake); + plaintext_producer.take().map(Waker::wake); + return Poll::Ready(Ok(StreamResult::Dropped)); + } + }; + + let mut dst = dst.as_direct(store, state.tls_bytes_to_write()); + if dst.remaining().is_empty() { + return Poll::Ready(Ok(StreamResult::Completed)); + } + let n = conn.write_tls(&mut dst)?; + debug_assert_ne!(n, 0); + if conn.wants_read() { + ciphertext_consumer.take().map(Waker::wake); + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +pub struct ResultProducer { + rx: oneshot::Receiver, + getter: for<'a> fn(&'a mut T) -> WasiTlsCtxView<'a>, +} + +impl FutureProducer for ResultProducer +where + D: 'static, +{ + type Item = Result<(), Resource>; + + fn poll_produce( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + mut store: StoreContextMut, + finish: bool, + ) -> Poll>> { + match Pin::new(&mut self.rx).poll(cx) { + Poll::Ready(Ok(err)) => { + let WasiTlsCtxView { table, .. } = (self.getter)(store.data_mut()); + let err = push_error(table, format!("{err}"))?; + Poll::Ready(Ok(Some(Err(err)))) + } + Poll::Ready(Err(..)) => Poll::Ready(Ok(Some(Ok(())))), + Poll::Pending if finish => Poll::Ready(Ok(None)), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/crates/wasi-tls/src/p3/host/types.rs b/crates/wasi-tls/src/p3/host/types.rs new file mode 100644 index 000000000000..98a11b69b41c --- /dev/null +++ b/crates/wasi-tls/src/p3/host/types.rs @@ -0,0 +1,21 @@ +use super::{mk_delete, mk_get}; +use crate::p3::WasiTlsCtxView; +use crate::p3::bindings::tls::types::{Error, Host, HostError}; +use wasmtime::component::Resource; + +mk_get!(Error, get_error, "error"); +mk_delete!(Error, delete_error, "error"); + +impl Host for WasiTlsCtxView<'_> {} + +impl HostError for WasiTlsCtxView<'_> { + fn to_debug_string(&mut self, err: Resource) -> wasmtime::Result { + let err = get_error(self.table, &err)?; + Ok(err.clone()) + } + + fn drop(&mut self, err: Resource) -> wasmtime::Result<()> { + delete_error(&mut self.table, err)?; + Ok(()) + } +} diff --git a/crates/wasi-tls/src/p3/mod.rs b/crates/wasi-tls/src/p3/mod.rs new file mode 100644 index 000000000000..fbab96748f45 --- /dev/null +++ b/crates/wasi-tls/src/p3/mod.rs @@ -0,0 +1,151 @@ +//! Experimental, unstable and incomplete implementation of wasip3 version of `wasi:tls`. +//! +//! This module is under heavy development. +//! It is not compliant with semver and is not ready +//! for production use. +//! +//! Bug and security fixes limited to wasip3 will not be given patch releases. +//! +//! Documentation of this module may be incorrect or out-of-sync with the implementation. + +pub mod bindings; +mod host; + +use crate::p3::host::{ + CiphertextConsumer, CiphertextProducer, PlaintextConsumer, PlaintextProducer, +}; +use bindings::tls::{client, types}; +use core::task::Waker; +use std::sync::{Arc, Mutex}; +use tokio::sync::oneshot; +use wasmtime::component::{HasData, Linker, ResourceTable}; + +/// The type for which this crate implements the `wasi:tls` interfaces. +pub struct WasiTls; + +impl HasData for WasiTls { + type Data<'a> = WasiTlsCtxView<'a>; +} + +/// A trait which provides internal WASI TLS state. +pub trait WasiTlsCtx: Send {} + +/// Default implementation of [WasiTlsCtx]. +#[derive(Clone, Default)] +pub struct DefaultWasiTlsCtx; + +impl WasiTlsCtx for DefaultWasiTlsCtx {} + +/// View into [WasiTlsCtx] implementation and [ResourceTable]. +pub struct WasiTlsCtxView<'a> { + /// Mutable reference to the WASI TLS context. + pub ctx: &'a mut dyn WasiTlsCtx, + + /// Mutable reference to table used to manage resources. + pub table: &'a mut ResourceTable, +} + +/// A trait which provides internal WASI TLS state. +pub trait WasiTlsView: Send { + /// Return a [WasiTlsCtxView] from mutable reference to self. + fn tls(&mut self) -> WasiTlsCtxView<'_>; +} + +/// Add all interfaces from this module into the `linker` provided. +/// +/// This function will add all interfaces implemented by this module to the +/// [`Linker`], which corresponds to the `wasi:tls/imports` world supported by +/// this module. +/// +/// # Example +/// +/// ``` +/// use wasmtime::{Engine, Result, Store, Config}; +/// use wasmtime::component::{Linker, ResourceTable}; +/// use wasmtime_wasi_tls::p3::{DefaultWasiTlsCtx, WasiTlsCtxView, WasiTlsView}; +/// +/// fn main() -> Result<()> { +/// let mut config = Config::new(); +/// config.wasm_component_model_async(true); +/// let engine = Engine::new(&config)?; +/// +/// let mut linker = Linker::::new(&engine); +/// wasmtime_wasi_tls::p3::add_to_linker(&mut linker)?; +/// // ... add any further functionality to `linker` if desired ... +/// +/// let mut store = Store::new( +/// &engine, +/// MyState::default(), +/// ); +/// +/// // ... use `linker` to instantiate within `store` ... +/// +/// Ok(()) +/// } +/// +/// #[derive(Default)] +/// struct MyState { +/// tls: DefaultWasiTlsCtx, +/// table: ResourceTable, +/// } +/// +/// impl WasiTlsView for MyState { +/// fn tls(&mut self) -> WasiTlsCtxView<'_> { +/// WasiTlsCtxView { +/// ctx: &mut self.tls, +/// table: &mut self.table, +/// } +/// } +/// } +/// ``` +pub fn add_to_linker(linker: &mut Linker) -> wasmtime::Result<()> +where + T: WasiTlsView + 'static, +{ + client::add_to_linker::<_, WasiTls>(linker, T::tls)?; + types::add_to_linker::<_, WasiTls>(linker, T::tls)?; + Ok(()) +} + +/// TLS client connector state. +#[derive(Default)] +pub struct Connector { + pub(crate) receive_tx: Option<( + oneshot::Sender>, + oneshot::Sender>, + oneshot::Sender, + )>, + pub(crate) send_tx: Option<( + oneshot::Sender>, + oneshot::Sender< + PlaintextConsumer, + >, + oneshot::Sender, + )>, +} + +type TlsStreamArc = Arc>>; + +struct TlsStream { + conn: T, + plaintext_consumer_dropped: bool, + ciphertext_consumer_dropped: bool, + ciphertext_consumer: Option, + ciphertext_producer: Option, + plaintext_consumer: Option, + plaintext_producer: Option, +} + +impl TlsStream { + fn new(conn: T) -> Self { + Self { + conn, + plaintext_consumer_dropped: false, + ciphertext_consumer_dropped: false, + plaintext_producer: None, + plaintext_consumer: None, + ciphertext_producer: None, + ciphertext_consumer: None, + } + } +} diff --git a/crates/wasi-tls/src/p3/wit/deps/tls/client.wit b/crates/wasi-tls/src/p3/wit/deps/tls/client.wit new file mode 100644 index 000000000000..8f282dbe8e36 --- /dev/null +++ b/crates/wasi-tls/src/p3/wit/deps/tls/client.wit @@ -0,0 +1,22 @@ +interface client { + use types.{error}; + + resource connector { + constructor(); + + /// Set up the encryption stream transform. + /// This takes an unprotected `cleartext` application data stream and + /// returns an encrypted data stream, ready to be sent out over the network. + /// Closing the `cleartext` stream will cause a `close_notify` packet to be emitted on the returned output stream. + send: func(cleartext: stream) -> tuple, future>>; + + /// Set up the decryption stream transform. + /// This takes an encrypted data stream, as received via e.g. the network, + /// and returns a decrypted application data stream. + receive: func(ciphertext: stream) -> tuple, future>>; + + /// Perform the handshake. + /// The `send` & `receive` streams must be set up before calling this method. + connect: static async func(this: connector, server-name: string) -> result<_, error>; + } +} diff --git a/crates/wasi-tls/src/p3/wit/deps/tls/types.wit b/crates/wasi-tls/src/p3/wit/deps/tls/types.wit new file mode 100644 index 000000000000..fc6c4b103e16 --- /dev/null +++ b/crates/wasi-tls/src/p3/wit/deps/tls/types.wit @@ -0,0 +1,5 @@ +interface types { + resource error { + to-debug-string: func() -> string; + } +} diff --git a/crates/wasi-tls/src/p3/wit/deps/tls/world.wit b/crates/wasi-tls/src/p3/wit/deps/tls/world.wit new file mode 100644 index 000000000000..599d49688ad2 --- /dev/null +++ b/crates/wasi-tls/src/p3/wit/deps/tls/world.wit @@ -0,0 +1,6 @@ +package wasi:tls@0.3.0-draft; + +world imports { + import client; + import types; +} diff --git a/crates/wasi-tls/src/p3/wit/world.wit b/crates/wasi-tls/src/p3/wit/world.wit new file mode 100644 index 000000000000..51e0e7e8cc58 --- /dev/null +++ b/crates/wasi-tls/src/p3/wit/world.wit @@ -0,0 +1,2 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi-tls; diff --git a/crates/wasi-tls/tests/main.rs b/crates/wasi-tls/tests/p2.rs similarity index 90% rename from crates/wasi-tls/tests/main.rs rename to crates/wasi-tls/tests/p2.rs index 86ddee7f888f..45d3e920c917 100644 --- a/crates/wasi-tls/tests/main.rs +++ b/crates/wasi-tls/tests/p2.rs @@ -60,9 +60,9 @@ macro_rules! assert_test_exists { }; } -test_programs_artifacts::foreach_tls!(assert_test_exists); +test_programs_artifacts::foreach_p2_tls!(assert_test_exists); #[tokio::test(flavor = "multi_thread")] -async fn tls_sample_application() -> Result<()> { - run_test(test_programs_artifacts::TLS_SAMPLE_APPLICATION_COMPONENT).await +async fn p2_tls_sample_application() -> Result<()> { + run_test(test_programs_artifacts::P2_TLS_SAMPLE_APPLICATION_COMPONENT).await } diff --git a/crates/wasi-tls/tests/p3.rs b/crates/wasi-tls/tests/p3.rs new file mode 100644 index 000000000000..e68c07df238c --- /dev/null +++ b/crates/wasi-tls/tests/p3.rs @@ -0,0 +1,85 @@ +#![cfg(feature = "p3")] + +use wasmtime::component::{Component, Linker, ResourceTable}; +use wasmtime::error::Context as _; +use wasmtime::{Result, Store, format_err}; +use wasmtime_wasi::p3::bindings::Command; +use wasmtime_wasi::{WasiCtx, WasiCtxView, WasiView}; +use wasmtime_wasi_tls::p3::{DefaultWasiTlsCtx, WasiTlsCtxView, WasiTlsView}; + +struct Ctx { + table: ResourceTable, + wasi_ctx: WasiCtx, + wasi_tls_ctx: DefaultWasiTlsCtx, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.table, + } + } +} + +impl WasiTlsView for Ctx { + fn tls(&mut self) -> WasiTlsCtxView<'_> { + WasiTlsCtxView { + ctx: &mut self.wasi_tls_ctx, + table: &mut self.table, + } + } +} + +async fn run_test(path: &str) -> Result<()> { + let ctx = Ctx { + table: ResourceTable::new(), + wasi_ctx: WasiCtx::builder() + .inherit_stdout() + .inherit_stderr() + .inherit_network() + .allow_ip_name_lookup(true) + .build(), + wasi_tls_ctx: DefaultWasiTlsCtx, + }; + + let engine = test_programs_artifacts::engine(|config| { + config.wasm_component_model_async(true); + }); + let mut store = Store::new(&engine, ctx); + + let mut linker = Linker::new(&engine); + // TODO: Remove once test components are not built for `wasm32-wasip1` + wasmtime_wasi::p2::add_to_linker_async(&mut linker) + .context("failed to link `wasi:cli@0.2.x`")?; + wasmtime_wasi::p3::add_to_linker(&mut linker).context("failed to link `wasi:cli@0.3.x`")?; + wasmtime_wasi_tls::p3::add_to_linker(&mut linker)?; + + let component = Component::from_file(&engine, path)?; + let command = Command::instantiate_async(&mut store, &component, &linker) + .await + .context("failed to instantiate `wasi:cli/command`")?; + let (res, task) = store + .run_concurrent(async move |store| command.wasi_cli_run().call_run(store).await) + .await + .context("failed to call `wasi:cli/run#run`")? + .context("guest trapped")?; + res.map_err(|()| format_err!("`wasi:cli/run#run` failed"))?; + store + .run_concurrent(async move |store| task.block(store).await) + .await +} + +macro_rules! assert_test_exists { + ($name:ident) => { + #[expect(unused_imports, reason = "just here to assert it exists")] + use self::$name as _; + }; +} + +test_programs_artifacts::foreach_p3_tls!(assert_test_exists); + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn p3_tls_sample_application() -> Result<()> { + run_test(test_programs_artifacts::P3_TLS_SAMPLE_APPLICATION_COMPONENT).await +} diff --git a/src/commands/run.rs b/src/commands/run.rs index d61c7fe62521..9af831e18a86 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -1092,6 +1092,10 @@ impl RunCommand { ctx.ctx().table, ) })?; + #[cfg(feature = "component-model-async")] + if self.run.common.wasi.p3.unwrap_or(crate::common::P3_DEFAULT) { + wasmtime_wasi_tls::p3::add_to_linker(linker)?; + } let ctx = wasmtime_wasi_tls::WasiTlsCtxBuilder::new().build(); store.data_mut().wasi_tls = Some(Arc::new(ctx)); @@ -1225,8 +1229,11 @@ pub struct Host { wasi_http_outgoing_body_buffer_chunks: Option, #[cfg(feature = "wasi-http")] wasi_http_outgoing_body_chunk_size: Option, - #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] - p3_http: crate::common::DefaultP3Ctx, + #[cfg(all( + any(feature = "wasi-http", feature = "wasi-tls"), + feature = "component-model-async" + ))] + p3_ctx: crate::common::DefaultP3Ctx, limits: StoreLimits, #[cfg(feature = "profiling")] guest_profiler: Option>, @@ -1286,7 +1293,17 @@ impl wasmtime_wasi_http::p3::WasiHttpView for Host { fn http(&mut self) -> wasmtime_wasi_http::p3::WasiHttpCtxView<'_> { wasmtime_wasi_http::p3::WasiHttpCtxView { table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, - ctx: &mut self.p3_http, + ctx: &mut self.p3_ctx, + } + } +} + +#[cfg(all(feature = "wasi-tls", feature = "component-model-async"))] +impl wasmtime_wasi_tls::p3::WasiTlsView for Host { + fn tls(&mut self) -> wasmtime_wasi_tls::p3::WasiTlsCtxView<'_> { + wasmtime_wasi_tls::p3::WasiTlsCtxView { + table: WasiView::ctx(unwrap_singlethread_context(&mut self.wasip1_ctx)).table, + ctx: &mut self.p3_ctx, } } } diff --git a/src/common.rs b/src/common.rs index 6f51c0f3e6d2..5448d5935198 100644 --- a/src/common.rs +++ b/src/common.rs @@ -433,7 +433,12 @@ impl Profile { } #[derive(Default, Clone)] -#[cfg(all(feature = "wasi-http", feature = "component-model-async"))] +#[cfg(all( + any(feature = "wasi-http", feature = "wasi-tls"), + feature = "component-model-async" +))] pub struct DefaultP3Ctx; #[cfg(all(feature = "wasi-http", feature = "component-model-async"))] impl wasmtime_wasi_http::p3::WasiHttpCtx for DefaultP3Ctx {} +#[cfg(all(feature = "wasi-tls", feature = "component-model-async"))] +impl wasmtime_wasi_tls::p3::WasiTlsCtx for DefaultP3Ctx {}