diff --git a/Cargo.lock b/Cargo.lock index 4999c5e69230..9112f1e1234e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,6 +261,18 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "cap-net-ext" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffc30dee200c20b4dcb80572226f42658e1d9c4b668656d7cc59c33d50e396e" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 0.38.8", + "smallvec", +] + [[package]] name = "cap-primitives" version = "2.0.0" @@ -273,7 +285,7 @@ dependencies = [ "io-lifetimes 2.0.2", "ipnet", "maybe-owned", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", "winx", ] @@ -297,7 +309,7 @@ dependencies = [ "cap-primitives", "io-extras", "io-lifetimes 2.0.2", - "rustix 0.38.4", + "rustix 0.38.8", ] [[package]] @@ -308,7 +320,7 @@ checksum = "7b9e3348a3510c4619b4c7a7bcdef09a71221da18f266bda3ed6b9aea2c509e2" dependencies = [ "cap-std", "rand", - "rustix 0.38.4", + "rustix 0.38.8", "uuid", ] @@ -320,7 +332,7 @@ checksum = "f8f52b3c8f4abfe3252fd0a071f3004aaa3b18936ec97bdbd8763ce03aff6247" dependencies = [ "cap-primitives", "once_cell", - "rustix 0.38.4", + "rustix 0.38.8", "winx", ] @@ -1120,7 +1132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b0377f1edc77dbd1118507bc7a66e4ab64d2b90c66f90726dc801e73a8c68f9" dependencies = [ "cfg-if", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -1184,7 +1196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" dependencies = [ "io-lifetimes 2.0.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -2303,9 +2315,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ "bitflags 2.3.3", "errno", @@ -2485,9 +2497,9 @@ checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" dependencies = [ "serde", ] @@ -2595,7 +2607,7 @@ dependencies = [ "cap-std", "fd-lock", "io-lifetimes 2.0.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", "winx", ] @@ -3012,7 +3024,7 @@ dependencies = [ "io-lifetimes 2.0.2", "is-terminal", "once_cell", - "rustix 0.38.4", + "rustix 0.38.8", "system-interface", "tempfile", "tracing", @@ -3030,7 +3042,7 @@ dependencies = [ "cap-std", "io-extras", "log", - "rustix 0.38.4", + "rustix 0.38.8", "thiserror", "tracing", "wasmtime", @@ -3058,6 +3070,14 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasi-sockets-tests" +version = "0.0.0" +dependencies = [ + "anyhow", + "wit-bindgen", +] + [[package]] name = "wasi-tests" version = "0.0.0" @@ -3076,7 +3096,7 @@ dependencies = [ "cap-tempfile", "io-extras", "io-lifetimes 2.0.2", - "rustix 0.38.4", + "rustix 0.38.8", "tempfile", "tokio", "wasi-cap-std-sync", @@ -3401,7 +3421,7 @@ dependencies = [ "log", "once_cell", "pretty_env_logger 0.5.0", - "rustix 0.38.4", + "rustix 0.38.8", "serde", "sha2", "tempfile", @@ -3431,7 +3451,7 @@ dependencies = [ "num_cpus", "once_cell", "rayon", - "rustix 0.38.4", + "rustix 0.38.8", "serde", "serde_json", "target-lexicon", @@ -3586,7 +3606,7 @@ dependencies = [ "backtrace", "cc", "cfg-if", - "rustix 0.38.4", + "rustix 0.38.8", "wasmtime-asm-macros", "wasmtime-versioned-export-macros", "windows-sys", @@ -3661,7 +3681,7 @@ dependencies = [ "log", "object", "rustc-demangle", - "rustix 0.38.4", + "rustix 0.38.8", "serde", "target-lexicon", "wasmtime-environ", @@ -3677,7 +3697,7 @@ version = "13.0.0" dependencies = [ "object", "once_cell", - "rustix 0.38.4", + "rustix 0.38.8", "wasmtime-versioned-export-macros", ] @@ -3707,7 +3727,7 @@ dependencies = [ "once_cell", "paste", "rand", - "rustix 0.38.4", + "rustix 0.38.8", "sptr", "wasm-encoder 0.31.1", "wasmtime-asm-macros", @@ -3747,16 +3767,18 @@ dependencies = [ "bitflags 2.3.3", "bytes", "cap-fs-ext", + "cap-net-ext", "cap-rand", "cap-std", "cap-time-ext", "fs-set-times", "futures", "io-extras", + "io-lifetimes 2.0.2", "is-terminal", "libc", "once_cell", - "rustix 0.38.4", + "rustix 0.38.8", "system-interface", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index a49b8fd53ba0..8bd17fd13dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ members = [ "crates/jit-icache-coherence", "crates/test-programs/wasi-tests", "crates/test-programs/wasi-http-tests", + "crates/test-programs/wasi-sockets-tests", "crates/test-programs/command-tests", "crates/test-programs/reactor-tests", "crates/wmemcheck", @@ -191,13 +192,14 @@ target-lexicon = { version = "0.12.3", default-features = false, features = ["st cap-std = "2.0.0" cap-rand = { version = "2.0.0", features = ["small_rng"] } cap-fs-ext = "2.0.0" +cap-net-ext = "2.0.0" cap-time-ext = "2.0.0" cap-tempfile = "2.0.0" 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.4" +rustix = "0.38.8" is-terminal = "0.4.0" # wit-bindgen: wit-bindgen = { version = "0.9.0", default-features = false } diff --git a/crates/test-programs/build.rs b/crates/test-programs/build.rs index 8d16007fc689..cc2a9ca3b467 100644 --- a/crates/test-programs/build.rs +++ b/crates/test-programs/build.rs @@ -30,6 +30,7 @@ fn build_and_generate_tests() { println!("cargo:rerun-if-changed=./wasi-tests"); println!("cargo:rerun-if-changed=./command-tests"); println!("cargo:rerun-if-changed=./reactor-tests"); + println!("cargo:rerun-if-changed=./wasi-sockets-tests"); if BUILD_WASI_HTTP_TESTS { println!("cargo:rerun-if-changed=./wasi-http-tests"); } else { @@ -43,6 +44,7 @@ fn build_and_generate_tests() { .arg("--package=wasi-tests") .arg("--package=command-tests") .arg("--package=reactor-tests") + .arg("--package=wasi-sockets-tests") .env("CARGO_TARGET_DIR", &out_dir) .env("CARGO_PROFILE_DEV_DEBUG", "1") .env_remove("CARGO_ENCODED_RUSTFLAGS"); @@ -64,6 +66,14 @@ fn build_and_generate_tests() { components_rs(&meta, "command-tests", "bin", &command_adapter, &out_dir); components_rs(&meta, "reactor-tests", "cdylib", &reactor_adapter, &out_dir); + + components_rs( + &meta, + "wasi-sockets-tests", + "bin", + &command_adapter, + &out_dir, + ); } // Creates an `${out_dir}/${package}_modules.rs` file that exposes a `get_module(&str) -> Module`, diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs new file mode 100644 index 000000000000..ff119108fc0c --- /dev/null +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -0,0 +1,93 @@ +#![cfg(all(feature = "test_programs", not(skip_wasi_sockets_tests)))] +use cap_std::ambient_authority; +use wasmtime::component::Linker; +use wasmtime::{Config, Engine, Store}; +use wasmtime_wasi::preview2::{self, command::Command, Table, WasiCtx, WasiCtxBuilder, WasiView}; + +lazy_static::lazy_static! { + static ref ENGINE: Engine = { + let mut config = Config::new(); + config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + config.wasm_component_model(true); + config.async_support(true); + + let engine = Engine::new(&config).unwrap(); + engine + }; +} +// uses ENGINE, creates a fn get_component(&str) -> Component +include!(concat!( + env!("OUT_DIR"), + "/wasi_sockets_tests_components.rs" +)); + +struct SocketsCtx { + table: Table, + wasi: WasiCtx, +} + +impl WasiView for SocketsCtx { + fn table(&self) -> &Table { + &self.table + } + fn table_mut(&mut self) -> &mut Table { + &mut self.table + } + fn ctx(&self) -> &WasiCtx { + &self.wasi + } + fn ctx_mut(&mut self) -> &mut WasiCtx { + &mut self.wasi + } +} + +async fn run(name: &str) -> anyhow::Result<()> { + let component = get_component(name); + let mut linker = Linker::new(&ENGINE); + + preview2::bindings::io::streams::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::poll::poll::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::exit::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::stdin::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::stdout::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::stderr::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::terminal_input::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::terminal_output::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::terminal_stdin::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::terminal_stdout::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::terminal_stderr::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::cli::environment::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::filesystem::types::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::filesystem::preopens::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::sockets::tcp::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::sockets::tcp_create_socket::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::sockets::network::add_to_linker(&mut linker, |x| x)?; + preview2::bindings::sockets::instance_network::add_to_linker(&mut linker, |x| x)?; + + // Create our wasi context. + let mut table = Table::new(); + let wasi = WasiCtxBuilder::new() + .inherit_stdio() + .inherit_network(ambient_authority()) + .arg(name) + .build(&mut table)?; + + let mut store = Store::new(&ENGINE, SocketsCtx { table, wasi }); + + let (command, _instance) = Command::instantiate_async(&mut store, &component, &linker).await?; + command + .wasi_cli_run() + .call_run(&mut store) + .await? + .map_err(|()| anyhow::anyhow!("command returned with failing exit status")) +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_v4() { + run("tcp_v4").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_v6() { + run("tcp_v6").await.unwrap(); +} diff --git a/crates/test-programs/wasi-sockets-tests/Cargo.toml b/crates/test-programs/wasi-sockets-tests/Cargo.toml new file mode 100644 index 000000000000..3dcc85370173 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasi-sockets-tests" +version = "0.0.0" +readme = "README.md" +edition = "2021" +publish = false + +[dependencies] +anyhow = { workspace = true } +wit-bindgen = { workspace = true, default-features = false, features = ["macros"] } diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs new file mode 100644 index 000000000000..907b8986b7de --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs @@ -0,0 +1,112 @@ +//! A simple TCP testcase, using IPv4. + +use wasi::io::streams; +use wasi::poll::poll; +use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress}; +use wasi::sockets::{instance_network, network, tcp, tcp_create_socket}; +use wasi_sockets_tests::*; + +fn wait(sub: poll::Pollable) { + loop { + let wait = poll::poll_oneoff(&[sub]); + if wait[0] { + break; + } + } +} + +fn main() { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let net = instance_network::instance_network(); + + let sock = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv4).unwrap(); + + let addr = IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, // use any free port + address: (127, 0, 0, 1), // localhost + }); + + let sub = tcp::subscribe(sock); + + tcp::start_bind(sock, net, addr).unwrap(); + wait(sub); + tcp::finish_bind(sock).unwrap(); + + tcp::start_listen(sock).unwrap(); + wait(sub); + tcp::finish_listen(sock).unwrap(); + + let addr = tcp::local_address(sock).unwrap(); + + let client = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv4).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = streams::write(client_output, &[]).unwrap(); + assert_eq!(n, 0); + assert_eq!(status, streams::StreamStatus::Open); + + let (n, status) = streams::write(client_output, first_message).unwrap(); + assert_eq!(n, first_message.len() as u64); // Not guaranteed to work but should work in practice. + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + + let (empty_data, status) = streams::read(input, 0).unwrap(); + assert!(empty_data.is_empty()); + assert_eq!(status, streams::StreamStatus::Open); + + let (data, status) = streams::read(input, first_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + tcp::drop_tcp_socket(accepted); + streams::drop_input_stream(input); + streams::drop_output_stream(output); + + // Check that we sent and recieved our message! + assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + + // Another client + let client = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv4).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = streams::write(client_output, second_message).unwrap(); + assert_eq!(n, second_message.len() as u64); // Not guaranteed to work but should work in practice. + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + let (data, status) = streams::read(input, second_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(input); + streams::drop_output_stream(output); + tcp::drop_tcp_socket(accepted); + + // Check that we sent and recieved our message! + assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + + poll::drop_pollable(sub); + tcp::drop_tcp_socket(sock); + network::drop_network(net); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs new file mode 100644 index 000000000000..47a569aed30c --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs @@ -0,0 +1,114 @@ +//! Like v4.rs, but with IPv6. + +use wasi::io::streams; +use wasi::poll::poll; +use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; +use wasi::sockets::{instance_network, network, tcp, tcp_create_socket}; +use wasi_sockets_tests::*; + +fn wait(sub: poll::Pollable) { + loop { + let wait = poll::poll_oneoff(&[sub]); + if wait[0] { + break; + } + } +} + +fn main() { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let net = instance_network::instance_network(); + + let sock = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv6).unwrap(); + + let addr = IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: 0, // use any free port + address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost + flow_info: 0, + scope_id: 0, + }); + + let sub = tcp::subscribe(sock); + + tcp::start_bind(sock, net, addr).unwrap(); + wait(sub); + tcp::finish_bind(sock).unwrap(); + + tcp::start_listen(sock).unwrap(); + wait(sub); + tcp::finish_listen(sock).unwrap(); + + let addr = tcp::local_address(sock).unwrap(); + + let client = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv6).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = streams::write(client_output, &[]).unwrap(); + assert_eq!(n, 0); + assert_eq!(status, streams::StreamStatus::Open); + + let (n, status) = streams::write(client_output, first_message).unwrap(); + assert_eq!(n, first_message.len() as u64); // Not guaranteed to work but should work in practice. + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + + let (empty_data, status) = streams::read(input, 0).unwrap(); + assert!(empty_data.is_empty()); + assert_eq!(status, streams::StreamStatus::Open); + + let (data, status) = streams::read(input, first_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + tcp::drop_tcp_socket(accepted); + streams::drop_input_stream(input); + streams::drop_output_stream(output); + + // Check that we sent and recieved our message! + assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + + // Another client + let client = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv6).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = streams::write(client_output, second_message).unwrap(); + assert_eq!(n, second_message.len() as u64); // Not guaranteed to work but should work in practice. + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + let (data, status) = streams::read(input, second_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(input); + streams::drop_output_stream(output); + tcp::drop_tcp_socket(accepted); + + // Check that we sent and recieved our message! + assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + + poll::drop_pollable(sub); + tcp::drop_tcp_socket(sock); + network::drop_network(net); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs new file mode 100644 index 000000000000..cf3ecf02f82c --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -0,0 +1 @@ +wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); diff --git a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit index 6c64b4617b98..f15d19d037da 100644 --- a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit +++ b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit @@ -5,9 +5,9 @@ interface ip-name-lookup { /// Resolve an internet host name to a list of IP addresses. - /// + /// /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. - /// + /// /// # Parameters /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted /// to ASCII using IDNA encoding. @@ -17,18 +17,18 @@ interface ip-name-lookup { /// systems without an active IPv6 interface. Notes: /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. - /// + /// /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream` /// that can be used to (asynchronously) fetch the results. - /// + /// /// At the moment, the stream never completes successfully with 0 items. Ie. the first call /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. - /// + /// /// # Typical errors /// - `invalid-name`: `name` is a syntactically invalid domain name. /// - `invalid-name`: `name` is an IP address. /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) - /// + /// /// # References: /// - /// - @@ -41,14 +41,14 @@ interface ip-name-lookup { type resolve-address-stream = u32 /// Returns the next address from the resolver. - /// + /// /// This function should be called multiple times. On each call, it will /// return the next address in connection order preference. If all /// addresses have been exhausted, this function returns `none`. /// After which, you should release the stream with `drop-resolve-address-stream`. - /// + /// /// This function never returns IPv4-mapped IPv6 addresses. - /// + /// /// # Typical errors /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) @@ -57,12 +57,12 @@ interface ip-name-lookup { resolve-next-address: func(this: resolve-address-stream) -> result, error-code> /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-resolve-address-stream: func(this: resolve-address-stream) /// Create a `pollable` which will resolve once the stream is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: resolve-address-stream) -> pollable diff --git a/crates/wasi-http/wit/deps/sockets/network.wit b/crates/wasi-http/wit/deps/sockets/network.wit index c370214ce1f9..a198ea8017de 100644 --- a/crates/wasi-http/wit/deps/sockets/network.wit +++ b/crates/wasi-http/wit/deps/sockets/network.wit @@ -4,12 +4,12 @@ interface network { /// An opaque resource that represents access to (a subset of) the network. /// This enables context-based security for networking. /// There is no need for this to map 1:1 to a physical network interface. - /// + /// /// FYI, In the future this will be replaced by handle types. type network = u32 /// Dispose of the specified `network`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-network: func(this: network) @@ -153,7 +153,7 @@ interface network { enum ip-address-family { /// Similar to `AF_INET` in POSIX. - ipv4, + ipv4, /// Similar to `AF_INET6` in POSIX. ipv6, diff --git a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit index f467d2856906..f43bc8979047 100644 --- a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit @@ -4,20 +4,20 @@ interface tcp-create-socket { use tcp.{tcp-socket} /// Create a new TCP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect` /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References /// - /// - diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit index 7ed46a690491..4edb1db7f0b1 100644 --- a/crates/wasi-http/wit/deps/sockets/tcp.wit +++ b/crates/wasi-http/wit/deps/sockets/tcp.wit @@ -6,7 +6,7 @@ interface tcp { /// A TCP socket handle. type tcp-socket = u32 - + enum shutdown-type { /// Similar to `SHUT_RD` in POSIX. @@ -25,24 +25,24 @@ interface tcp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will /// implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -52,11 +52,11 @@ interface tcp { finish-bind: func(this: tcp-socket) -> result<_, error-code> /// Connect to a remote endpoint. - /// + /// /// On success: /// - the socket is transitioned into the Connection state /// - a pair of streams is returned that can be used to read & write to the connection - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) @@ -65,7 +65,7 @@ interface tcp { /// - `already-connected`: The socket is already in the Connection state. (EISCONN) /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) @@ -74,7 +74,7 @@ interface tcp { /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -84,13 +84,15 @@ interface tcp { finish-connect: func(this: tcp-socket) -> result, error-code> /// Start listening for new connections. - /// + /// /// Transitions the socket into the Listener state. - /// - /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// + /// Unlike POSIX: + /// - this function is async. This enables interactive WASI hosts to inject permission prompts. + /// - the socket must already be explicitly bound. + /// /// # Typical `start` errors - /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `listen` must be identical to the one passed to `bind`. + /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) /// - `already-listening`: The socket is already in the Listener state. /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) @@ -105,22 +107,22 @@ interface tcp { /// - /// - /// - - start-listen: func(this: tcp-socket, network: network) -> result<_, error-code> + start-listen: func(this: tcp-socket) -> result<_, error-code> finish-listen: func(this: tcp-socket) -> result<_, error-code> /// Accept a new client socket. - /// + /// /// The returned socket is bound and in the Connection state. - /// + /// /// On success, this function returns the newly accepted client socket along with /// a pair of streams that can be used to read & write to the connection. - /// + /// /// # Typical errors /// - `not-listening`: Socket is not in the Listener state. (EINVAL) /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// Host implementations must skip over transient errors returned by the native accept syscall. - /// + /// /// # References /// - /// - @@ -129,10 +131,10 @@ interface tcp { accept: func(this: tcp-socket) -> result, error-code> /// Get the bound local address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -141,10 +143,10 @@ interface tcp { local-address: func(this: tcp-socket) -> result /// Get the bound remote address. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -153,14 +155,14 @@ interface tcp { remote-address: func(this: tcp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: tcp-socket) -> ip-address-family - + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -170,28 +172,28 @@ interface tcp { set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Hints the desired listen queue size. Implementations are free to ignore this. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) keep-alive: func(this: tcp-socket) -> result set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) no-delay: func(this: tcp-socket) -> result set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> - + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -200,16 +202,16 @@ interface tcp { set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -220,25 +222,25 @@ interface tcp { set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: tcp-socket) -> pollable /// Initiate a graceful shutdown. - /// + /// /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. /// Any data still in the receive queue at time of calling `shutdown` will be discarded. /// - send: the socket is not expecting to send any more data to the peer. All subsequent write /// operations on the `output-stream` associated with this socket will return an error. /// - both: same effect as receive & send combined. - /// + /// /// The shutdown function does not close (drop) the socket. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -247,9 +249,9 @@ interface tcp { shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error-code> /// Dispose of the specified `tcp-socket`, after which it may no longer be used. - /// + /// /// Similar to the POSIX `close` function. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-tcp-socket: func(this: tcp-socket) } diff --git a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit index 1cfbd7f0bdd8..cd4c08fb1000 100644 --- a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit @@ -4,20 +4,20 @@ interface udp-create-socket { use udp.{udp-socket} /// Create a new UDP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` is called, /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References: /// - /// - diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit index 9dd4573bd17c..700b9e247692 100644 --- a/crates/wasi-http/wit/deps/sockets/udp.wit +++ b/crates/wasi-http/wit/deps/sockets/udp.wit @@ -27,23 +27,23 @@ interface udp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to connect will implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -53,29 +53,29 @@ interface udp { finish-bind: func(this: udp-socket) -> result<_, error-code> /// Set the destination address. - /// + /// /// The local-address is updated based on the best network path to `remote-address`. - /// + /// /// When a destination address is set: /// - all receive operations will only return datagrams sent from the provided `remote-address`. /// - the `send` function can only be used to send to this destination. - /// + /// /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -84,32 +84,42 @@ interface udp { start-connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> finish-connect: func(this: udp-socket) -> result<_, error-code> - /// Receive a message. - /// - /// Returns: - /// - The sender address of the datagram - /// - The number of bytes read. - /// + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// If `max-results` is 0, this function returns successfully with an empty list. + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. (EINVAL) /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - receive: func(this: udp-socket) -> result - - /// Send a message to a specific destination address. - /// + receive: func(this: udp-socket, max-results: u64) -> result, error-code> + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// /// The remote address option is required. To send a message to the "connected" peer, /// call `remote-address` to get their address. - /// + /// /// # Typical errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) @@ -119,22 +129,23 @@ interface udp { /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - send: func(this: udp-socket, datagram: datagram) -> result<_, error-code> + send: func(this: udp-socket, datagrams: list) -> result /// Get the current bound address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -143,10 +154,10 @@ interface udp { local-address: func(this: udp-socket) -> result /// Get the address set with `connect`. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -155,14 +166,14 @@ interface udp { remote-address: func(this: udp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: udp-socket) -> ip-address-family /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -172,25 +183,23 @@ interface udp { set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) unicast-hop-limit: func(this: udp-socket) -> result set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// - /// Fails when this socket is in the Listening state. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) receive-buffer-size: func(this: udp-socket) -> result @@ -199,13 +208,13 @@ interface udp { set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: udp-socket) -> pollable /// Dispose of the specified `udp-socket`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-udp-socket: func(this: udp-socket) } diff --git a/crates/wasi-http/wit/test.wit b/crates/wasi-http/wit/test.wit index 447304cba3d8..4543cb194af1 100644 --- a/crates/wasi-http/wit/test.wit +++ b/crates/wasi-http/wit/test.wit @@ -26,3 +26,16 @@ world test-command { import wasi:cli/stdout import wasi:cli/stderr } + +world test-command-with-sockets { + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:sockets/tcp + import wasi:sockets/tcp-create-socket + import wasi:sockets/network + import wasi:sockets/instance-network +} diff --git a/crates/wasi-preview1-component-adapter/src/descriptors.rs b/crates/wasi-preview1-component-adapter/src/descriptors.rs index 81d8f2810771..bcc1ba7cf71b 100644 --- a/crates/wasi-preview1-component-adapter/src/descriptors.rs +++ b/crates/wasi-preview1-component-adapter/src/descriptors.rs @@ -47,10 +47,10 @@ impl Drop for Descriptor { /// identifies what kind of stream they are and possibly supporting /// type-specific operations like seeking. pub struct Streams { - /// The output stream, if present. + /// The input stream, if present. pub input: Cell>, - /// The input stream, if present. + /// The output stream, if present. pub output: Cell>, /// Information about the source of the stream. diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 462dccbaeea8..6817470e9eda 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -29,7 +29,9 @@ tracing = { workspace = true, optional = true } cap-std = { workspace = true, optional = true } cap-rand = { workspace = true, optional = true } cap-fs-ext = { workspace = true, optional = true } +cap-net-ext = { workspace = true, optional = true } cap-time-ext = { workspace = true, optional = true } +io-lifetimes = { workspace = true, optional = true } fs-set-times = { workspace = true, optional = true } is-terminal = { workspace = true, optional = true } bitflags = { workspace = true, optional = true } @@ -41,7 +43,7 @@ futures = { workspace = true, optional = true } tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros"] } [target.'cfg(unix)'.dependencies] -rustix = { workspace = true, features = ["fs"], optional = true } +rustix = { workspace = true, features = ["event", "fs", "net"], optional = true } [target.'cfg(unix)'.dev-dependencies] libc = { workspace = true } @@ -49,6 +51,7 @@ libc = { workspace = true } [target.'cfg(windows)'.dependencies] io-extras = { workspace = true } windows-sys = { workspace = true } +rustix = { workspace = true, features = ["event", "net"], optional = true } [features] default = ["sync", "preview2", "preview1-on-preview2"] @@ -63,7 +66,9 @@ preview2 = [ 'dep:cap-std', 'dep:cap-rand', 'dep:cap-fs-ext', + 'dep:cap-net-ext', 'dep:cap-time-ext', + 'dep:io-lifetimes', 'dep:fs-set-times', 'dep:is-terminal', 'dep:bitflags', diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs index 76120de6a7da..ae01eeb0219d 100644 --- a/crates/wasi/src/preview2/command.rs +++ b/crates/wasi/src/preview2/command.rs @@ -6,10 +6,12 @@ wasmtime::component::bindgen!({ async: true, trappable_error_type: { "wasi:filesystem/types"::"error-code": Error, + "wasi:sockets/tcp"::"error-code": Error, }, with: { "wasi:filesystem/types": crate::preview2::bindings::filesystem::types, "wasi:filesystem/preopens": crate::preview2::bindings::filesystem::preopens, + "wasi:sockets/tcp": crate::preview2::bindings::sockets::tcp, "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, "wasi:poll/poll": crate::preview2::bindings::poll::poll, "wasi:io/streams": crate::preview2::bindings::io::streams, @@ -35,6 +37,7 @@ pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> any crate::preview2::bindings::clocks::timezone::add_to_linker(l, |t| t)?; crate::preview2::bindings::filesystem::types::add_to_linker(l, |t| t)?; crate::preview2::bindings::filesystem::preopens::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::tcp::add_to_linker(l, |t| t)?; crate::preview2::bindings::poll::poll::add_to_linker(l, |t| t)?; crate::preview2::bindings::io::streams::add_to_linker(l, |t| t)?; crate::preview2::bindings::random::random::add_to_linker(l, |t| t)?; @@ -60,10 +63,12 @@ pub mod sync { async: false, trappable_error_type: { "wasi:filesystem/types"::"error-code": Error, + "wasi:sockets/tcp"::"error-code": Error, }, with: { "wasi:filesystem/types": crate::preview2::bindings::sync_io::filesystem::types, "wasi:filesystem/preopens": crate::preview2::bindings::filesystem::preopens, + "wasi:sockets/tcp": crate::preview2::bindings::sockets::tcp, "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, "wasi:poll/poll": crate::preview2::bindings::sync_io::poll::poll, "wasi:io/streams": crate::preview2::bindings::sync_io::io::streams, @@ -104,6 +109,7 @@ pub mod sync { crate::preview2::bindings::cli::terminal_stdin::add_to_linker(l, |t| t)?; crate::preview2::bindings::cli::terminal_stdout::add_to_linker(l, |t| t)?; crate::preview2::bindings::cli::terminal_stderr::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::tcp::add_to_linker(l, |t| t)?; Ok(()) } } diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 252307c80faf..03b69da3b795 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -8,7 +8,11 @@ use crate::preview2::{ DirPerms, FilePerms, IsATTY, Table, }; use cap_rand::{Rng, RngCore, SeedableRng}; +use cap_std::ipnet::{self, IpNet}; +use cap_std::net::Pool; +use cap_std::{ambient_authority, AmbientAuthority}; use std::mem; +use std::net::{Ipv4Addr, Ipv6Addr}; pub struct WasiCtxBuilder { stdin: (Box, IsATTY), @@ -18,6 +22,7 @@ pub struct WasiCtxBuilder { args: Vec, preopens: Vec<(Dir, String)>, + pool: Pool, random: Box, insecure_random: Box, insecure_random_seed: u128, @@ -63,6 +68,7 @@ impl WasiCtxBuilder { env: Vec::new(), args: Vec::new(), preopens: Vec::new(), + pool: Pool::new(), random: random::thread_rng(), insecure_random, insecure_random_seed, @@ -200,6 +206,57 @@ impl WasiCtxBuilder { self } + /// Add all network addresses accessable to the host to the pool. + pub fn inherit_network(&mut self, ambient_authority: AmbientAuthority) -> &mut Self { + self.pool.insert_ip_net_port_any( + IpNet::new(Ipv4Addr::UNSPECIFIED.into(), 0).unwrap(), + ambient_authority, + ); + self.pool.insert_ip_net_port_any( + IpNet::new(Ipv6Addr::UNSPECIFIED.into(), 0).unwrap(), + ambient_authority, + ); + self + } + + /// Add network addresses to the pool. + pub fn insert_addr(&mut self, addrs: A) -> std::io::Result<()> { + self.pool.insert(addrs, ambient_authority()) + } + + /// Add a specific [`cap_std::net::SocketAddr`] to the pool. + pub fn insert_socket_addr(&mut self, addr: cap_std::net::SocketAddr) { + self.pool.insert_socket_addr(addr, ambient_authority()); + } + + /// Add a range of network addresses, accepting any port, to the pool. + /// + /// Unlike `insert_ip_net`, this function grants access to any requested port. + pub fn insert_ip_net_port_any(&mut self, ip_net: ipnet::IpNet) { + self.pool + .insert_ip_net_port_any(ip_net, ambient_authority()) + } + + /// Add a range of network addresses, accepting a range of ports, to + /// per-instance networks. + /// + /// This grants access to the port range starting at `ports_start` and, if + /// `ports_end` is provided, ending before `ports_end`. + pub fn insert_ip_net_port_range( + &mut self, + ip_net: ipnet::IpNet, + ports_start: u16, + ports_end: Option, + ) { + self.pool + .insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority()) + } + + /// Add a range of network addresses with a specific port to the pool. + pub fn insert_ip_net(&mut self, ip_net: ipnet::IpNet, port: u16) { + self.pool.insert_ip_net(ip_net, port, ambient_authority()) + } + /// Uses the configured context so far to construct the final `WasiCtx`. /// /// This will insert resources into the provided `table`. @@ -221,6 +278,7 @@ impl WasiCtxBuilder { env, args, preopens, + pool, random, insecure_random, insecure_random_seed, @@ -260,6 +318,7 @@ impl WasiCtxBuilder { env, args, preopens, + pool, random, insecure_random, insecure_random_seed, @@ -288,4 +347,5 @@ pub struct WasiCtx { pub(crate) stdin: StdioInput, pub(crate) stdout: StdioOutput, pub(crate) stderr: StdioOutput, + pub(crate) pool: Pool, } diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index c9224ce33fcf..53d8362a3178 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -12,7 +12,9 @@ bitflags::bitflags! { pub(crate) struct File { /// Wrapped in an Arc because the same underlying file is used for - /// implementing the stream types. Also needed for [`block`]. + /// implementing the stream types. Also needed for [`spawn_blocking`]. + /// + /// [`spawn_blocking`]: Self::spawn_blocking pub file: Arc, pub perms: FilePerms, } diff --git a/crates/wasi/src/preview2/host/instance_network.rs b/crates/wasi/src/preview2/host/instance_network.rs new file mode 100644 index 000000000000..8c8b56974aa1 --- /dev/null +++ b/crates/wasi/src/preview2/host/instance_network.rs @@ -0,0 +1,11 @@ +use crate::preview2::bindings::sockets::instance_network::{self, Network}; +use crate::preview2::network::{HostNetwork, TableNetworkExt}; +use crate::preview2::WasiView; + +impl instance_network::Host for T { + fn instance_network(&mut self) -> Result { + let network = HostNetwork::new(self.ctx().pool.clone()); + let network = self.table_mut().push_network(network)?; + Ok(network) + } +} diff --git a/crates/wasi/src/preview2/host/mod.rs b/crates/wasi/src/preview2/host/mod.rs index 8bb111cc3a21..138166731565 100644 --- a/crates/wasi/src/preview2/host/mod.rs +++ b/crates/wasi/src/preview2/host/mod.rs @@ -2,5 +2,9 @@ mod clocks; mod env; mod exit; pub(crate) mod filesystem; +mod instance_network; mod io; +mod network; mod random; +mod tcp; +mod tcp_create_socket; diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs new file mode 100644 index 000000000000..023a5d9f3026 --- /dev/null +++ b/crates/wasi/src/preview2/host/network.rs @@ -0,0 +1,186 @@ +use crate::preview2::bindings::sockets::network::{ + self, ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4Address, Ipv4SocketAddress, Ipv6Address, + Ipv6SocketAddress, +}; +use crate::preview2::network::TableNetworkExt; +use crate::preview2::{TableError, WasiView}; +use std::io; + +impl network::Host for T { + fn drop_network(&mut self, this: network::Network) -> Result<(), anyhow::Error> { + let table = self.table_mut(); + + table.delete_network(this)?; + + Ok(()) + } +} + +impl From for network::Error { + fn from(error: TableError) -> Self { + Self::trap(error.into()) + } +} + +impl From for network::Error { + fn from(error: io::Error) -> Self { + match error.kind() { + // Errors that we can directly map. + io::ErrorKind::PermissionDenied => ErrorCode::AccessDenied, + io::ErrorKind::ConnectionRefused => ErrorCode::ConnectionRefused, + io::ErrorKind::ConnectionReset => ErrorCode::ConnectionReset, + io::ErrorKind::NotConnected => ErrorCode::NotConnected, + io::ErrorKind::AddrInUse => ErrorCode::AddressInUse, + io::ErrorKind::AddrNotAvailable => ErrorCode::AddressNotBindable, + io::ErrorKind::WouldBlock => ErrorCode::WouldBlock, + io::ErrorKind::TimedOut => ErrorCode::Timeout, + io::ErrorKind::Unsupported => ErrorCode::NotSupported, + io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory, + + // Errors we don't expect to see here. + io::ErrorKind::Interrupted | io::ErrorKind::ConnectionAborted => { + // Transient errors should be skipped. + return Self::trap(error.into()); + } + + // Errors not expected from network APIs. + io::ErrorKind::WriteZero + | io::ErrorKind::InvalidInput + | io::ErrorKind::InvalidData + | io::ErrorKind::BrokenPipe + | io::ErrorKind::NotFound + | io::ErrorKind::UnexpectedEof + | io::ErrorKind::AlreadyExists => return Self::trap(error.into()), + + // Errors that don't correspond to a Rust `io::ErrorKind`. + io::ErrorKind::Other => match error.raw_os_error() { + None => return Self::trap(error.into()), + Some(libc::ENOBUFS) | Some(libc::ENOMEM) => ErrorCode::OutOfMemory, + Some(libc::EOPNOTSUPP) => ErrorCode::NotSupported, + Some(libc::ENETUNREACH) | Some(libc::EHOSTUNREACH) | Some(libc::ENETDOWN) => { + ErrorCode::RemoteUnreachable + } + Some(libc::ECONNRESET) => ErrorCode::ConnectionReset, + Some(libc::ECONNREFUSED) => ErrorCode::ConnectionRefused, + Some(libc::EADDRINUSE) => ErrorCode::AddressInUse, + Some(_) => return Self::trap(error.into()), + }, + _ => return Self::trap(error.into()), + } + .into() + } +} + +impl From for network::Error { + fn from(error: rustix::io::Errno) -> Self { + std::io::Error::from(error).into() + } +} + +impl From for std::net::SocketAddr { + fn from(addr: IpSocketAddress) -> Self { + match addr { + IpSocketAddress::Ipv4(ipv4) => Self::V4(ipv4.into()), + IpSocketAddress::Ipv6(ipv6) => Self::V6(ipv6.into()), + } + } +} + +impl From for IpSocketAddress { + fn from(addr: std::net::SocketAddr) -> Self { + match addr { + std::net::SocketAddr::V4(v4) => Self::Ipv4(v4.into()), + std::net::SocketAddr::V6(v6) => Self::Ipv6(v6.into()), + } + } +} + +impl From for std::net::SocketAddrV4 { + fn from(addr: Ipv4SocketAddress) -> Self { + Self::new(to_ipv4_addr(addr.address), addr.port) + } +} + +impl From for Ipv4SocketAddress { + fn from(addr: std::net::SocketAddrV4) -> Self { + Self { + address: from_ipv4_addr(*addr.ip()), + port: addr.port(), + } + } +} + +impl From for std::net::SocketAddrV6 { + fn from(addr: Ipv6SocketAddress) -> Self { + Self::new( + to_ipv6_addr(addr.address), + addr.port, + addr.flow_info, + addr.scope_id, + ) + } +} + +impl From for Ipv6SocketAddress { + fn from(addr: std::net::SocketAddrV6) -> Self { + Self { + address: from_ipv6_addr(*addr.ip()), + port: addr.port(), + flow_info: addr.flowinfo(), + scope_id: addr.scope_id(), + } + } +} + +fn to_ipv4_addr(addr: Ipv4Address) -> std::net::Ipv4Addr { + let (x0, x1, x2, x3) = addr; + std::net::Ipv4Addr::new(x0, x1, x2, x3) +} + +fn from_ipv4_addr(addr: std::net::Ipv4Addr) -> Ipv4Address { + let [x0, x1, x2, x3] = addr.octets(); + (x0, x1, x2, x3) +} + +fn to_ipv6_addr(addr: Ipv6Address) -> std::net::Ipv6Addr { + let (x0, x1, x2, x3, x4, x5, x6, x7) = addr; + std::net::Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7) +} + +fn from_ipv6_addr(addr: std::net::Ipv6Addr) -> Ipv6Address { + let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments(); + (x0, x1, x2, x3, x4, x5, x6, x7) +} + +impl std::net::ToSocketAddrs for IpSocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddr::from(*self).to_socket_addrs() + } +} + +impl std::net::ToSocketAddrs for Ipv4SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddrV4::from(*self).to_socket_addrs() + } +} + +impl std::net::ToSocketAddrs for Ipv6SocketAddress { + type Iter = ::Iter; + + fn to_socket_addrs(&self) -> io::Result { + std::net::SocketAddrV6::from(*self).to_socket_addrs() + } +} + +impl From for cap_net_ext::AddressFamily { + fn from(family: IpAddressFamily) -> Self { + match family { + IpAddressFamily::Ipv4 => cap_net_ext::AddressFamily::Ipv4, + IpAddressFamily::Ipv6 => cap_net_ext::AddressFamily::Ipv6, + } + } +} diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs new file mode 100644 index 000000000000..a00cc2f863af --- /dev/null +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -0,0 +1,530 @@ +use crate::preview2::bindings::{ + io::streams::{InputStream, OutputStream}, + poll::poll::Pollable, + sockets::network::{self, ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::tcp::{self, ShutdownType}, +}; +use crate::preview2::network::TableNetworkExt; +use crate::preview2::poll::TablePollableExt; +use crate::preview2::stream::TableStreamExt; +use crate::preview2::tcp::{HostTcpSocket, HostTcpState, TableTcpSocketExt}; +use crate::preview2::{HostPollable, PollableFuture, WasiView}; +use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; +use io_lifetimes::AsSocketlike; +use rustix::io::Errno; +use rustix::net::sockopt; +use std::any::Any; +#[cfg(unix)] +use tokio::io::Interest; +#[cfg(not(unix))] +use tokio::task::spawn_blocking; + +impl tcp::Host for T { + fn start_bind( + &mut self, + this: tcp::TcpSocket, + network: Network, + local_address: IpSocketAddress, + ) -> Result<(), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Default => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let network = table.get_network(network)?; + let binder = network.0.tcp_binder(local_address)?; + + // Perform the OS bind call. + binder.bind_existing_tcp_listener(socket.tcp_socket())?; + + let socket = table.get_tcp_socket_mut(this)?; + socket.tcp_state = HostTcpState::BindStarted; + + Ok(()) + } + + fn finish_bind(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; + + match socket.tcp_state { + HostTcpState::BindStarted => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + socket.tcp_state = HostTcpState::Bound; + + Ok(()) + } + + fn start_connect( + &mut self, + this: tcp::TcpSocket, + network: Network, + remote_address: IpSocketAddress, + ) -> Result<(), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Default => {} + HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let network = table.get_network(network)?; + let connecter = network.0.tcp_connecter(remote_address)?; + + // Do an OS `connect`. Our socket is non-blocking, so it'll either... + match connecter.connect_existing_tcp_listener(socket.tcp_socket()) { + // succeed immediately, + Ok(()) => { + let socket = table.get_tcp_socket_mut(this)?; + socket.tcp_state = HostTcpState::ConnectReady; + return Ok(()); + } + // continue in progress, + Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} + // or fail immediately. + Err(err) => return Err(err.into()), + } + + let socket = table.get_tcp_socket_mut(this)?; + socket.tcp_state = HostTcpState::Connecting; + + Ok(()) + } + + fn finish_connect( + &mut self, + this: tcp::TcpSocket, + ) -> Result<(InputStream, OutputStream), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; + + match socket.tcp_state { + HostTcpState::ConnectReady => {} + HostTcpState::Connecting => { + // Do a `poll` to test for completion, using a timeout of zero + // to avoid blocking. + match rustix::event::poll( + &mut [rustix::event::PollFd::new( + socket.tcp_socket(), + rustix::event::PollFlags::OUT, + )], + 0, + ) { + Ok(0) => return Err(ErrorCode::WouldBlock.into()), + Ok(_) => (), + Err(err) => Err(err).unwrap(), + } + + // Check whether the connect succeeded. + match sockopt::get_socket_error(socket.tcp_socket()) { + Ok(Ok(())) => {} + Err(err) | Ok(Err(err)) => return Err(err.into()), + } + } + _ => return Err(ErrorCode::NotInProgress.into()), + }; + + socket.tcp_state = HostTcpState::Connected; + + let input_clone = socket.clone_inner(); + let output_clone = socket.clone_inner(); + + let input_stream = self.table_mut().push_input_stream(Box::new(input_clone))?; + let output_stream = self + .table_mut() + .push_output_stream(Box::new(output_clone))?; + + Ok((input_stream, output_stream)) + } + + fn start_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; + + match socket.tcp_state { + HostTcpState::Bound => {} + HostTcpState::ListenStarted => return Err(ErrorCode::AlreadyListening.into()), + HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + _ => return Err(ErrorCode::NotInProgress.into()), + } + + socket.tcp_socket().listen(None)?; + + socket.tcp_state = HostTcpState::ListenStarted; + + Ok(()) + } + + fn finish_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; + + match socket.tcp_state { + HostTcpState::ListenStarted => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + socket.tcp_state = HostTcpState::Listening; + + Ok(()) + } + + fn accept( + &mut self, + this: tcp::TcpSocket, + ) -> Result<(tcp::TcpSocket, InputStream, OutputStream), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Listening => {} + HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + _ => return Err(ErrorCode::NotInProgress.into()), + } + + // Do the OS accept call. + let (connection, _addr) = socket.tcp_socket().accept_with(Blocking::No)?; + let tcp_socket = HostTcpSocket::from_tcp_stream(connection)?; + + let input_clone = tcp_socket.clone_inner(); + let output_clone = tcp_socket.clone_inner(); + + let tcp_socket = self.table_mut().push_tcp_socket(tcp_socket)?; + let input_stream = self.table_mut().push_input_stream(Box::new(input_clone))?; + let output_stream = self + .table_mut() + .push_output_stream(Box::new(output_clone))?; + + Ok((tcp_socket, input_stream, output_stream)) + } + + fn local_address(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + let addr = socket + .tcp_socket() + .as_socketlike_view::() + .local_addr()?; + Ok(addr.into()) + } + + fn remote_address(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + let addr = socket + .tcp_socket() + .as_socketlike_view::() + .peer_addr()?; + Ok(addr.into()) + } + + fn address_family(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + // If `SO_DOMAIN` is available, use it. + // + // TODO: OpenBSD also supports this; upstream PRs are posted. + #[cfg(not(any( + windows, + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + { + use rustix::net::AddressFamily; + + let family = sockopt::get_socket_domain(socket.tcp_socket())?; + let family = match family { + AddressFamily::INET => IpAddressFamily::Ipv4, + AddressFamily::INET6 => IpAddressFamily::Ipv6, + _ => return Err(ErrorCode::NotSupported.into()), + }; + Ok(family) + } + + // When `SO_DOMAIN` is not available, emulate it. + #[cfg(any( + windows, + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if let Ok(_) = sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { + return Ok(IpAddressFamily::Ipv6); + } + if let Ok(_) = sockopt::get_ip_ttl(socket.tcp_socket()) { + return Ok(IpAddressFamily::Ipv4); + } + Err(ErrorCode::NotSupported.into()) + } + } + + fn ipv6_only(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?) + } + + fn set_ipv6_only(&mut self, this: tcp::TcpSocket, value: bool) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?) + } + + fn set_listen_backlog_size( + &mut self, + this: tcp::TcpSocket, + value: u64, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Listening => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(rustix::net::listen(socket.tcp_socket(), value)?) + } + + fn keep_alive(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::get_socket_keepalive(socket.tcp_socket())?) + } + + fn set_keep_alive(&mut self, this: tcp::TcpSocket, value: bool) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::set_socket_keepalive(socket.tcp_socket(), value)?) + } + + fn no_delay(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::get_tcp_nodelay(socket.tcp_socket())?) + } + + fn set_no_delay(&mut self, this: tcp::TcpSocket, value: bool) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::set_tcp_nodelay(socket.tcp_socket(), value)?) + } + + fn unicast_hop_limit(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + // We don't track whether the socket is IPv4 or IPv6 so try one and + // fall back to the other. + match sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { + Ok(value) => Ok(value), + Err(Errno::NOPROTOOPT) => { + let value = sockopt::get_ip_ttl(socket.tcp_socket())?; + let value = value.try_into().unwrap(); + Ok(value) + } + Err(err) => Err(err.into()), + } + } + + fn set_unicast_hop_limit( + &mut self, + this: tcp::TcpSocket, + value: u8, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + // We don't track whether the socket is IPv4 or IPv6 so try one and + // fall back to the other. + match sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value)) { + Ok(()) => Ok(()), + Err(Errno::NOPROTOOPT) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), + Err(err) => Err(err.into()), + } + } + + fn receive_buffer_size(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::get_socket_recv_buffer_size(socket.tcp_socket())? as u64) + } + + fn set_receive_buffer_size( + &mut self, + this: tcp::TcpSocket, + value: u64, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_recv_buffer_size( + socket.tcp_socket(), + value, + )?) + } + + fn send_buffer_size(&mut self, this: tcp::TcpSocket) -> Result { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + Ok(sockopt::get_socket_send_buffer_size(socket.tcp_socket())? as u64) + } + + fn set_send_buffer_size( + &mut self, + this: tcp::TcpSocket, + value: u64, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_send_buffer_size( + socket.tcp_socket(), + value, + )?) + } + + fn subscribe(&mut self, this: tcp::TcpSocket) -> anyhow::Result { + fn make_tcp_socket_future<'a>(stream: &'a mut dyn Any) -> PollableFuture<'a> { + let socket = stream + .downcast_mut::() + .expect("downcast to HostTcpSocket failed"); + + // Some states are ready immediately. + match socket.tcp_state { + HostTcpState::BindStarted + | HostTcpState::ListenStarted + | HostTcpState::ConnectReady => return Box::pin(async { Ok(()) }), + _ => {} + } + + // FIXME: Add `Interest::ERROR` when we update to tokio 1.32. + #[cfg(unix)] + let join = Box::pin(async move { + socket + .inner + .tcp_socket + .ready(Interest::READABLE | Interest::WRITABLE) + .await + .unwrap() + .retain_ready(); + Ok(()) + }); + + #[cfg(not(unix))] + let join = Box::pin(async move { + let clone = socket.clone_inner(); + spawn_blocking(move || loop { + #[cfg(not(windows))] + let poll_flags = rustix::event::PollFlags::IN + | rustix::event::PollFlags::OUT + | rustix::event::PollFlags::ERR + | rustix::event::PollFlags::HUP; + // Windows doesn't appear to support `HUP`, or `ERR` + // combined with `IN`/`OUT`. + #[cfg(windows)] + let poll_flags = rustix::event::PollFlags::IN | rustix::event::PollFlags::OUT; + match rustix::event::poll( + &mut [rustix::event::PollFd::new(&clone.tcp_socket, poll_flags)], + -1, + ) { + Ok(_) => break, + Err(Errno::INTR) => (), + Err(err) => Err(err).unwrap(), + } + }) + .await + .unwrap(); + + Ok(()) + }); + + join + } + + let pollable = HostPollable::TableEntry { + index: this, + make_future: make_tcp_socket_future, + }; + + Ok(self.table_mut().push_host_pollable(pollable)?) + } + + fn shutdown( + &mut self, + this: tcp::TcpSocket, + shutdown_type: ShutdownType, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let how = match shutdown_type { + ShutdownType::Receive => std::net::Shutdown::Read, + ShutdownType::Send => std::net::Shutdown::Write, + ShutdownType::Both => std::net::Shutdown::Both, + }; + + socket + .tcp_socket() + .as_socketlike_view::() + .shutdown(how)?; + Ok(()) + } + + fn drop_tcp_socket(&mut self, this: tcp::TcpSocket) -> Result<(), anyhow::Error> { + let table = self.table_mut(); + + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let dropped = table.delete_tcp_socket(this)?; + + // If we might have an `event::poll` waiting on the socket, wake it up. + #[cfg(not(unix))] + { + match dropped.tcp_state { + HostTcpState::Default + | HostTcpState::BindStarted + | HostTcpState::Bound + | HostTcpState::ListenStarted + | HostTcpState::ConnectReady => {} + + HostTcpState::Listening | HostTcpState::Connecting | HostTcpState::Connected => { + match rustix::net::shutdown( + &dropped.inner.tcp_socket, + rustix::net::Shutdown::ReadWrite, + ) { + Ok(()) | Err(Errno::NOTCONN) => {} + Err(err) => Err(err).unwrap(), + } + } + } + } + + drop(dropped); + + Ok(()) + } +} + +// On POSIX, non-blocking TCP socket `connect` uses `EINPROGRESS`. +// +#[cfg(not(windows))] +const INPROGRESS: Errno = Errno::INPROGRESS; + +// On Windows, non-blocking TCP socket `connect` uses `WSAEWOULDBLOCK`. +// +#[cfg(windows)] +const INPROGRESS: Errno = Errno::WOULDBLOCK; diff --git a/crates/wasi/src/preview2/host/tcp_create_socket.rs b/crates/wasi/src/preview2/host/tcp_create_socket.rs new file mode 100644 index 000000000000..d2559e7df7d5 --- /dev/null +++ b/crates/wasi/src/preview2/host/tcp_create_socket.rs @@ -0,0 +1,18 @@ +use crate::preview2::bindings::{ + sockets::network::{self, IpAddressFamily}, + sockets::tcp::TcpSocket, + sockets::tcp_create_socket, +}; +use crate::preview2::tcp::{HostTcpSocket, TableTcpSocketExt}; +use crate::preview2::WasiView; + +impl tcp_create_socket::Host for T { + fn create_tcp_socket( + &mut self, + address_family: IpAddressFamily, + ) -> Result { + let socket = HostTcpSocket::new(address_family.into())?; + let socket = self.table_mut().push_tcp_socket(socket)?; + Ok(socket) + } +} diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index ac3a6576ec57..2fcf78a7efe6 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -21,6 +21,7 @@ mod ctx; mod error; mod filesystem; mod host; +mod network; pub mod pipe; mod poll; #[cfg(feature = "preview1-on-preview2")] @@ -29,6 +30,7 @@ mod random; mod stdio; mod stream; mod table; +mod tcp; pub use self::clocks::{HostMonotonicClock, HostWallClock}; pub use self::ctx::{WasiCtx, WasiCtxBuilder, WasiView}; @@ -117,10 +119,14 @@ pub mod bindings { import wasi:cli/terminal-stdin import wasi:cli/terminal-stdout import wasi:cli/terminal-stderr + import wasi:sockets/tcp + import wasi:sockets/tcp-create-socket + import wasi:sockets/instance-network ", tracing: true, trappable_error_type: { "wasi:filesystem/types"::"error-code": Error, + "wasi:sockets/network"::"error-code": Error, }, with: { "wasi:clocks/wall-clock": crate::preview2::bindings::clocks::wall_clock, @@ -131,7 +137,7 @@ pub mod bindings { }); } - pub use self::_internal_rest::wasi::{cli, random}; + pub use self::_internal_rest::wasi::{cli, random, sockets}; pub mod filesystem { pub use super::_internal_io::wasi::filesystem::types; pub use super::_internal_rest::wasi::filesystem::preopens; diff --git a/crates/wasi/src/preview2/network.rs b/crates/wasi/src/preview2/network.rs new file mode 100644 index 000000000000..4d462fcbd275 --- /dev/null +++ b/crates/wasi/src/preview2/network.rs @@ -0,0 +1,32 @@ +use crate::preview2::{Table, TableError}; +use cap_std::net::Pool; + +pub(crate) struct HostNetwork(pub(crate) Pool); + +impl HostNetwork { + pub fn new(pool: Pool) -> Self { + Self(pool) + } +} + +pub(crate) trait TableNetworkExt { + fn push_network(&mut self, network: HostNetwork) -> Result; + fn delete_network(&mut self, fd: u32) -> Result; + fn is_network(&self, fd: u32) -> bool; + fn get_network(&self, fd: u32) -> Result<&HostNetwork, TableError>; +} + +impl TableNetworkExt for Table { + fn push_network(&mut self, network: HostNetwork) -> Result { + self.push(Box::new(network)) + } + fn delete_network(&mut self, fd: u32) -> Result { + self.delete(fd) + } + fn is_network(&self, fd: u32) -> bool { + self.is::(fd) + } + fn get_network(&self, fd: u32) -> Result<&HostNetwork, TableError> { + self.get(fd) + } +} diff --git a/crates/wasi/src/preview2/stdio/unix.rs b/crates/wasi/src/preview2/stdio/unix.rs index a27cf381db63..e43e64b8d6dc 100644 --- a/crates/wasi/src/preview2/stdio/unix.rs +++ b/crates/wasi/src/preview2/stdio/unix.rs @@ -9,7 +9,7 @@ use std::pin::Pin; use std::sync::{Arc, Mutex, OnceLock}; use std::task::{Context, Poll}; use tokio::io::unix::AsyncFd; -use tokio::io::{AsyncRead, ReadBuf}; +use tokio::io::{AsyncRead, Interest, ReadBuf}; // We need a single global instance of the AsyncFd because creating // this instance registers the process's stdin fd with epoll, which will @@ -128,7 +128,7 @@ impl InnerStdin { } Ok(Self { - inner: AsyncFd::new(stdin)?, + inner: AsyncFd::with_interest(stdin, Interest::READABLE)?, }) } } diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs new file mode 100644 index 000000000000..46acafc027f5 --- /dev/null +++ b/crates/wasi/src/preview2/tcp.rs @@ -0,0 +1,270 @@ +use crate::preview2::{HostInputStream, HostOutputStream, StreamState, Table, TableError}; +use bytes::{Bytes, BytesMut}; +use cap_net_ext::{AddressFamily, Blocking, TcpListenerExt}; +use cap_std::net::{TcpListener, TcpStream}; +use io_lifetimes::AsSocketlike; +use std::io; +use std::sync::Arc; +use system_interface::io::IoExt; + +/// The state of a TCP socket. +/// +/// This represents the various states a socket can be in during the +/// activities of binding, listening, accepting, and connecting. +pub(crate) enum HostTcpState { + /// The initial state for a newly-created socket. + Default, + + /// Binding started via `start_bind`. + BindStarted, + + /// Binding finished via `finish_bind`. The socket has an address but + /// is not yet listening for connections. + Bound, + + /// Listening started via `listen_start`. + ListenStarted, + + /// The socket is now listening and waiting for an incoming connection. + Listening, + + /// An outgoing connection is started via `start_connect`. + Connecting, + + /// An outgoing connection is ready to be established. + ConnectReady, + + /// An outgoing connection has been established. + Connected, +} + +/// A host TCP socket, plus associated bookkeeping. +/// +/// The inner state is wrapped in an Arc because the same underlying socket is +/// used for implementing the stream types. Also needed for [`spawn_blocking`]. +/// +/// [`spawn_blocking`]: Self::spawn_blocking +pub(crate) struct HostTcpSocket { + /// The part of a `HostTcpSocket` which is reference-counted so that we + /// can pass it to async tasks. + pub(crate) inner: Arc, + + /// The current state in the bind/listen/accept/connect progression. + pub(crate) tcp_state: HostTcpState, +} + +/// The inner reference-counted state of a `HostTcpSocket`. +pub(crate) struct HostTcpSocketInner { + /// On Unix-family platforms we can use `AsyncFd` for efficient polling. + #[cfg(unix)] + pub(crate) tcp_socket: tokio::io::unix::AsyncFd, + + /// On non-Unix, we can use plain `poll`. + #[cfg(not(unix))] + pub(crate) tcp_socket: cap_std::net::TcpListener, +} + +impl HostTcpSocket { + /// Create a new socket in the given family. + pub fn new(family: AddressFamily) -> io::Result { + // Create a new host socket and set it to non-blocking, which is needed + // by our async implementation. + let tcp_socket = TcpListener::new(family, Blocking::No)?; + + // On Unix, pack it up in an `AsyncFd` so we can efficiently poll it. + #[cfg(unix)] + let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; + + Ok(Self { + inner: Arc::new(HostTcpSocketInner { tcp_socket }), + tcp_state: HostTcpState::Default, + }) + } + + /// Create a `HostTcpSocket` from an existing socket. + /// + /// The socket must be in non-blocking mode. + pub fn from_tcp_stream(tcp_socket: cap_std::net::TcpStream) -> io::Result { + let fd = rustix::fd::OwnedFd::from(tcp_socket); + let tcp_socket = TcpListener::from(fd); + + // On Unix, pack it up in an `AsyncFd` so we can efficiently poll it. + #[cfg(unix)] + let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; + + Ok(Self { + inner: Arc::new(HostTcpSocketInner { tcp_socket }), + tcp_state: HostTcpState::Default, + }) + } + + pub fn tcp_socket(&self) -> &cap_std::net::TcpListener { + self.inner.tcp_socket() + } + + pub fn clone_inner(&self) -> Arc { + Arc::clone(&self.inner) + } +} + +impl HostTcpSocketInner { + pub fn tcp_socket(&self) -> &cap_std::net::TcpListener { + let tcp_socket = &self.tcp_socket; + + // Unpack the `AsyncFd`. + #[cfg(unix)] + let tcp_socket = tcp_socket.get_ref(); + + tcp_socket + } + + /// Spawn a task on tokio's blocking thread for performing blocking + /// syscalls on the underlying [`cap_std::net::TcpListener`]. + #[cfg(not(unix))] + pub(crate) async fn spawn_blocking(self: &Arc, body: F) -> R + where + F: FnOnce(&cap_std::net::TcpListener) -> R + Send + 'static, + R: Send + 'static, + { + let s = Arc::clone(self); + tokio::task::spawn_blocking(move || body(s.tcp_socket())) + .await + .unwrap() + } +} + +#[async_trait::async_trait] +impl HostInputStream for Arc { + fn read(&mut self, size: usize) -> anyhow::Result<(Bytes, StreamState)> { + if size == 0 { + return Ok((Bytes::new(), StreamState::Open)); + } + let mut buf = BytesMut::zeroed(size); + let r = self + .tcp_socket() + .as_socketlike_view::() + .read(&mut buf); + let (n, state) = read_result(r)?; + buf.truncate(n); + Ok((buf.freeze(), state)) + } + + async fn ready(&mut self) -> anyhow::Result<()> { + #[cfg(unix)] + { + self.tcp_socket.readable().await?.retain_ready(); + Ok(()) + } + + #[cfg(not(unix))] + { + self.spawn_blocking(move |tcp_socket| { + match rustix::event::poll( + &mut [rustix::event::PollFd::new( + tcp_socket, + rustix::event::PollFlags::IN + | rustix::event::PollFlags::ERR + | rustix::event::PollFlags::HUP, + )], + -1, + ) { + Ok(_) => Ok(()), + Err(err) => Err(err.into()), + } + }) + .await + } + } +} + +#[async_trait::async_trait] +impl HostOutputStream for Arc { + fn write(&mut self, buf: Bytes) -> anyhow::Result<(usize, StreamState)> { + if buf.is_empty() { + return Ok((0, StreamState::Open)); + } + let r = self + .tcp_socket + .as_socketlike_view::() + .write(buf.as_ref()); + let (n, state) = write_result(r)?; + Ok((n, state)) + } + + async fn ready(&mut self) -> anyhow::Result<()> { + #[cfg(unix)] + { + self.tcp_socket.writable().await?.retain_ready(); + Ok(()) + } + + #[cfg(not(unix))] + { + self.spawn_blocking(move |tcp_socket| { + match rustix::event::poll( + &mut [rustix::event::PollFd::new( + tcp_socket, + rustix::event::PollFlags::OUT + | rustix::event::PollFlags::ERR + | rustix::event::PollFlags::HUP, + )], + -1, + ) { + Ok(_) => Ok(()), + Err(err) => Err(err.into()), + } + }) + .await + } + } +} + +pub(crate) trait TableTcpSocketExt { + fn push_tcp_socket(&mut self, tcp_socket: HostTcpSocket) -> Result; + fn delete_tcp_socket(&mut self, fd: u32) -> Result; + fn is_tcp_socket(&self, fd: u32) -> bool; + fn get_tcp_socket(&self, fd: u32) -> Result<&HostTcpSocket, TableError>; + fn get_tcp_socket_mut(&mut self, fd: u32) -> Result<&mut HostTcpSocket, TableError>; +} + +impl TableTcpSocketExt for Table { + fn push_tcp_socket(&mut self, tcp_socket: HostTcpSocket) -> Result { + self.push(Box::new(tcp_socket)) + } + fn delete_tcp_socket(&mut self, fd: u32) -> Result { + self.delete(fd) + } + fn is_tcp_socket(&self, fd: u32) -> bool { + self.is::(fd) + } + fn get_tcp_socket(&self, fd: u32) -> Result<&HostTcpSocket, TableError> { + self.get(fd) + } + fn get_tcp_socket_mut(&mut self, fd: u32) -> Result<&mut HostTcpSocket, TableError> { + self.get_mut(fd) + } +} + +pub(crate) fn read_result(r: io::Result) -> io::Result<(usize, StreamState)> { + match r { + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n, StreamState::Open)), + Err(e) if e.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), + Err(e) => Err(e), + } +} + +pub(crate) fn write_result(r: io::Result) -> io::Result<(usize, StreamState)> { + match r { + // We special-case zero-write stores ourselves, so if we get a zero + // back from a `write`, it means the stream is closed on some + // platforms. + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n, StreamState::Open)), + #[cfg(not(windows))] + Err(e) if e.raw_os_error() == Some(rustix::io::Errno::PIPE.raw_os_error()) => { + Ok((0, StreamState::Closed)) + } + Err(e) => Err(e), + } +} diff --git a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit index 6c64b4617b98..f15d19d037da 100644 --- a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit +++ b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit @@ -5,9 +5,9 @@ interface ip-name-lookup { /// Resolve an internet host name to a list of IP addresses. - /// + /// /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. - /// + /// /// # Parameters /// - `name`: The name to look up. IP addresses are not allowed. Unicode domain names are automatically converted /// to ASCII using IDNA encoding. @@ -17,18 +17,18 @@ interface ip-name-lookup { /// systems without an active IPv6 interface. Notes: /// - Even when no public IPv6 interfaces are present or active, names like "localhost" can still resolve to an IPv6 address. /// - Whatever is "available" or "unavailable" is volatile and can change everytime a network cable is unplugged. - /// + /// /// This function never blocks. It either immediately fails or immediately returns successfully with a `resolve-address-stream` /// that can be used to (asynchronously) fetch the results. - /// + /// /// At the moment, the stream never completes successfully with 0 items. Ie. the first call /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. - /// + /// /// # Typical errors /// - `invalid-name`: `name` is a syntactically invalid domain name. /// - `invalid-name`: `name` is an IP address. /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) - /// + /// /// # References: /// - /// - @@ -41,14 +41,14 @@ interface ip-name-lookup { type resolve-address-stream = u32 /// Returns the next address from the resolver. - /// + /// /// This function should be called multiple times. On each call, it will /// return the next address in connection order preference. If all /// addresses have been exhausted, this function returns `none`. /// After which, you should release the stream with `drop-resolve-address-stream`. - /// + /// /// This function never returns IPv4-mapped IPv6 addresses. - /// + /// /// # Typical errors /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) @@ -57,12 +57,12 @@ interface ip-name-lookup { resolve-next-address: func(this: resolve-address-stream) -> result, error-code> /// Dispose of the specified `resolve-address-stream`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-resolve-address-stream: func(this: resolve-address-stream) /// Create a `pollable` which will resolve once the stream is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: resolve-address-stream) -> pollable diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index c370214ce1f9..a198ea8017de 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -4,12 +4,12 @@ interface network { /// An opaque resource that represents access to (a subset of) the network. /// This enables context-based security for networking. /// There is no need for this to map 1:1 to a physical network interface. - /// + /// /// FYI, In the future this will be replaced by handle types. type network = u32 /// Dispose of the specified `network`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-network: func(this: network) @@ -153,7 +153,7 @@ interface network { enum ip-address-family { /// Similar to `AF_INET` in POSIX. - ipv4, + ipv4, /// Similar to `AF_INET6` in POSIX. ipv6, diff --git a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit index f467d2856906..f43bc8979047 100644 --- a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit @@ -4,20 +4,20 @@ interface tcp-create-socket { use tcp.{tcp-socket} /// Create a new TCP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`listen`/`connect` /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References /// - /// - diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 7ed46a690491..4edb1db7f0b1 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -6,7 +6,7 @@ interface tcp { /// A TCP socket handle. type tcp-socket = u32 - + enum shutdown-type { /// Similar to `SHUT_RD` in POSIX. @@ -25,24 +25,24 @@ interface tcp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to a listen or connect operation will /// implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -52,11 +52,11 @@ interface tcp { finish-bind: func(this: tcp-socket) -> result<_, error-code> /// Connect to a remote endpoint. - /// + /// /// On success: /// - the socket is transitioned into the Connection state /// - a pair of streams is returned that can be used to read & write to the connection - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) @@ -65,7 +65,7 @@ interface tcp { /// - `already-connected`: The socket is already in the Connection state. (EISCONN) /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) @@ -74,7 +74,7 @@ interface tcp { /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -84,13 +84,15 @@ interface tcp { finish-connect: func(this: tcp-socket) -> result, error-code> /// Start listening for new connections. - /// + /// /// Transitions the socket into the Listener state. - /// - /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// + /// Unlike POSIX: + /// - this function is async. This enables interactive WASI hosts to inject permission prompts. + /// - the socket must already be explicitly bound. + /// /// # Typical `start` errors - /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `listen` must be identical to the one passed to `bind`. + /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) /// - `already-listening`: The socket is already in the Listener state. /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) @@ -105,22 +107,22 @@ interface tcp { /// - /// - /// - - start-listen: func(this: tcp-socket, network: network) -> result<_, error-code> + start-listen: func(this: tcp-socket) -> result<_, error-code> finish-listen: func(this: tcp-socket) -> result<_, error-code> /// Accept a new client socket. - /// + /// /// The returned socket is bound and in the Connection state. - /// + /// /// On success, this function returns the newly accepted client socket along with /// a pair of streams that can be used to read & write to the connection. - /// + /// /// # Typical errors /// - `not-listening`: Socket is not in the Listener state. (EINVAL) /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// Host implementations must skip over transient errors returned by the native accept syscall. - /// + /// /// # References /// - /// - @@ -129,10 +131,10 @@ interface tcp { accept: func(this: tcp-socket) -> result, error-code> /// Get the bound local address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -141,10 +143,10 @@ interface tcp { local-address: func(this: tcp-socket) -> result /// Get the bound remote address. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -153,14 +155,14 @@ interface tcp { remote-address: func(this: tcp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: tcp-socket) -> ip-address-family - + /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -170,28 +172,28 @@ interface tcp { set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Hints the desired listen queue size. Implementations are free to ignore this. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) keep-alive: func(this: tcp-socket) -> result set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) no-delay: func(this: tcp-socket) -> result set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> - + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -200,16 +202,16 @@ interface tcp { set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. @@ -220,25 +222,25 @@ interface tcp { set-send-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: tcp-socket) -> pollable /// Initiate a graceful shutdown. - /// + /// /// - receive: the socket is not expecting to receive any more data from the peer. All subsequent read /// operations on the `input-stream` associated with this socket will return an End Of Stream indication. /// Any data still in the receive queue at time of calling `shutdown` will be discarded. /// - send: the socket is not expecting to send any more data to the peer. All subsequent write /// operations on the `output-stream` associated with this socket will return an error. /// - both: same effect as receive & send combined. - /// + /// /// The shutdown function does not close (drop) the socket. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -247,9 +249,9 @@ interface tcp { shutdown: func(this: tcp-socket, shutdown-type: shutdown-type) -> result<_, error-code> /// Dispose of the specified `tcp-socket`, after which it may no longer be used. - /// + /// /// Similar to the POSIX `close` function. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-tcp-socket: func(this: tcp-socket) } diff --git a/crates/wasi/wit/deps/sockets/udp-create-socket.wit b/crates/wasi/wit/deps/sockets/udp-create-socket.wit index 1cfbd7f0bdd8..cd4c08fb1000 100644 --- a/crates/wasi/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/udp-create-socket.wit @@ -4,20 +4,20 @@ interface udp-create-socket { use udp.{udp-socket} /// Create a new UDP socket. - /// + /// /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. - /// + /// /// This function does not require a network capability handle. This is considered to be safe because /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` is called, /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. - /// + /// /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. - /// + /// /// # Typical errors /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - /// + /// /// # References: /// - /// - diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index 9dd4573bd17c..700b9e247692 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -27,23 +27,23 @@ interface udp { /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which /// network interface(s) to bind to. /// If the TCP/UDP port is zero, the socket will be bound to a random free port. - /// + /// /// When a socket is not explicitly bound, the first invocation to connect will implicitly bind the socket. - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -53,29 +53,29 @@ interface udp { finish-bind: func(this: udp-socket) -> result<_, error-code> /// Set the destination address. - /// + /// /// The local-address is updated based on the best network path to `remote-address`. - /// + /// /// When a destination address is set: /// - all receive operations will only return datagrams sent from the provided `remote-address`. /// - the `send` function can only be used to send to this destination. - /// + /// /// Note that this function does not generate any network traffic and the peer is not aware of this "connection". - /// + /// /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. - /// + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) - /// + /// /// # Typical `finish` errors /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - @@ -84,32 +84,42 @@ interface udp { start-connect: func(this: udp-socket, network: network, remote-address: ip-socket-address) -> result<_, error-code> finish-connect: func(this: udp-socket) -> result<_, error-code> - /// Receive a message. - /// - /// Returns: - /// - The sender address of the datagram - /// - The number of bytes read. - /// + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// If `max-results` is 0, this function returns successfully with an empty list. + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. (EINVAL) /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - receive: func(this: udp-socket) -> result - - /// Send a message to a specific destination address. - /// + receive: func(this: udp-socket, max-results: u64) -> result, error-code> + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// /// The remote address option is required. To send a message to the "connected" peer, /// call `remote-address` to get their address. - /// + /// /// # Typical errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) @@ -119,22 +129,23 @@ interface udp { /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) - /// + /// /// # References /// - /// - /// - + /// - /// - /// - /// - /// - - send: func(this: udp-socket, datagram: datagram) -> result<_, error-code> + send: func(this: udp-socket, datagrams: list) -> result /// Get the current bound address. - /// + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. - /// + /// /// # References /// - /// - @@ -143,10 +154,10 @@ interface udp { local-address: func(this: udp-socket) -> result /// Get the address set with `connect`. - /// + /// /// # Typical errors /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) - /// + /// /// # References /// - /// - @@ -155,14 +166,14 @@ interface udp { remote-address: func(this: udp-socket) -> result /// Whether this is a IPv4 or IPv6 socket. - /// + /// /// Equivalent to the SO_DOMAIN socket option. address-family: func(this: udp-socket) -> ip-address-family /// Whether IPv4 compatibility (dual-stack) mode is disabled or not. - /// + /// /// Equivalent to the IPV6_V6ONLY socket option. - /// + /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. @@ -172,25 +183,23 @@ interface udp { set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) unicast-hop-limit: func(this: udp-socket) -> result set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> /// The kernel buffer space reserved for sends/receives on this socket. - /// + /// /// Note #1: an implementation may choose to cap or round the buffer size when setting the value. /// In other words, after setting a value, reading the same setting back may return a different value. - /// + /// /// Note #2: there is not necessarily a direct relationship between the kernel buffer size and the bytes of /// actual data to be sent/received by the application, because the kernel might also use the buffer space /// for internal metadata structures. - /// - /// Fails when this socket is in the Listening state. - /// + /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// + /// /// # Typical errors /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) receive-buffer-size: func(this: udp-socket) -> result @@ -199,13 +208,13 @@ interface udp { set-send-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> /// Create a `pollable` which will resolve once the socket is ready for I/O. - /// + /// /// Note: this function is here for WASI Preview2 only. /// It's planned to be removed when `future` is natively supported in Preview3. subscribe: func(this: udp-socket) -> pollable /// Dispose of the specified `udp-socket`, after which it may no longer be used. - /// + /// /// Note: this function is scheduled to be removed when Resources are natively supported in Wit. drop-udp-socket: func(this: udp-socket) } diff --git a/crates/wasi/wit/test.wit b/crates/wasi/wit/test.wit index 447304cba3d8..4543cb194af1 100644 --- a/crates/wasi/wit/test.wit +++ b/crates/wasi/wit/test.wit @@ -26,3 +26,16 @@ world test-command { import wasi:cli/stdout import wasi:cli/stderr } + +world test-command-with-sockets { + import wasi:poll/poll + import wasi:io/streams + import wasi:cli/environment + import wasi:cli/stdin + import wasi:cli/stdout + import wasi:cli/stderr + import wasi:sockets/tcp + import wasi:sockets/tcp-create-socket + import wasi:sockets/network + import wasi:sockets/instance-network +} diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 3532b776b806..79ceb44e75fd 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -1861,6 +1861,19 @@ few minor issues related to Stacked Borrows and running in MIRI. No fundamental change to any preexisting unsafe code is happening here. """ +[[audits.smallvec]] +who = "Dan Gohman " +criteria = "safe-to-deploy" +delta = "1.8.0 -> 1.11.0" +notes = """ +The main change is the switch to use `NonNull` internally instead of +`*mut T`. This seems reasonable, as `Vec` also never stores a null pointer, +and in particular the new `NonNull::new_unchecked`s look ok. + +Most of the rest of the changes are adding some new unstable features which +aren't enabled by default. +""" + [[audits.socket2]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -2877,6 +2890,12 @@ user-id = 6825 # Dan Gohman (sunfishcode) start = "2020-12-11" end = "2024-07-14" +[[trusted.cap-net-ext]] +criteria = "safe-to-deploy" +user-id = 6825 # Dan Gohman (sunfishcode) +start = "2020-12-11" +end = "2024-07-14" + [[trusted.cap-primitives]] criteria = "safe-to-deploy" user-id = 6825 # Dan Gohman (sunfishcode) diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 683f4da23a82..28b251f7ac1d 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -549,7 +549,7 @@ version = "1.8.0" criteria = "safe-to-deploy" [[exemptions.socket2]] -version = "0.4.4" +version = "0.4.9" criteria = "safe-to-deploy" [[exemptions.souper-ir]] diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index f22240289090..cfbc093764b6 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -288,6 +288,13 @@ user-id = 6825 user-login = "sunfishcode" user-name = "Dan Gohman" +[[publisher.cap-net-ext]] +version = "2.0.0" +when = "2023-06-30" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + [[publisher.cap-primitives]] version = "2.0.0" when = "2023-06-30" @@ -621,6 +628,13 @@ user-id = 6825 user-login = "sunfishcode" user-name = "Dan Gohman" +[[publisher.rustix]] +version = "0.38.8" +when = "2023-08-10" +user-id = 6825 +user-login = "sunfishcode" +user-name = "Dan Gohman" + [[publisher.ryu]] version = "1.0.9" when = "2021-12-12"