From 008e55045ed4daa84b3cc52d4b366cd098f6b1f9 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 3 Aug 2023 15:29:07 -0700 Subject: [PATCH 01/16] Implement the `tcp` interface of wasi-sockets. Implement the `tcp`, `tcp-create-socket`, and `network` interfaces of wasi-sockets. --- Cargo.lock | 64 +- Cargo.toml | 4 +- crates/test-programs/build.rs | 10 + crates/test-programs/tests/wasi-sockets.rs | 93 +++ .../wasi-sockets-tests/Cargo.toml | 10 + .../wasi-sockets-tests/src/bin/tcp_v4.rs | 99 +++ .../wasi-sockets-tests/src/bin/tcp_v6.rs | 101 +++ .../wasi-sockets-tests/src/lib.rs | 1 + .../src/descriptors.rs | 4 +- crates/wasi/Cargo.toml | 6 +- crates/wasi/src/preview2/command.rs | 6 + crates/wasi/src/preview2/ctx.rs | 60 ++ crates/wasi/src/preview2/filesystem.rs | 4 +- .../src/preview2/host/instance_network.rs | 11 + crates/wasi/src/preview2/host/mod.rs | 4 + crates/wasi/src/preview2/host/network.rs | 185 ++++++ crates/wasi/src/preview2/host/tcp.rs | 575 ++++++++++++++++++ .../src/preview2/host/tcp_create_socket.rs | 18 + crates/wasi/src/preview2/mod.rs | 8 +- crates/wasi/src/preview2/network.rs | 32 + crates/wasi/src/preview2/stdio/unix.rs | 4 +- crates/wasi/src/preview2/tcp.rs | 290 +++++++++ crates/wasi/wit/test.wit | 13 + supply-chain/audits.toml | 19 + supply-chain/imports.lock | 14 + 25 files changed, 1606 insertions(+), 29 deletions(-) create mode 100644 crates/test-programs/tests/wasi-sockets.rs create mode 100644 crates/test-programs/wasi-sockets-tests/Cargo.toml create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs create mode 100644 crates/test-programs/wasi-sockets-tests/src/lib.rs create mode 100644 crates/wasi/src/preview2/host/instance_network.rs create mode 100644 crates/wasi/src/preview2/host/network.rs create mode 100644 crates/wasi/src/preview2/host/tcp.rs create mode 100644 crates/wasi/src/preview2/host/tcp_create_socket.rs create mode 100644 crates/wasi/src/preview2/network.rs create mode 100644 crates/wasi/src/preview2/tcp.rs 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..fff3d6a09093 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs @@ -0,0 +1,99 @@ +//! 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, net).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, first_message).unwrap(); + assert_eq!(n, first_message.len() as u64); // Not guaranteed to work but should work in practice. + + 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, first_message.len() as u64).unwrap(); + + 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. + + 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(); + + 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..64b89957cde4 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs @@ -0,0 +1,101 @@ +//! 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, net).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, first_message).unwrap(); + assert_eq!(n, first_message.len() as u64); // Not guaranteed to work but should work in practice. + + 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, first_message.len() as u64).unwrap(); + + 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. + + 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(); + + 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-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..5cd5602d3fda 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 = ["fs", "net"], optional = true } [target.'cfg(unix)'.dev-dependencies] libc = { workspace = true } @@ -63,7 +65,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..5f6b090d6802 --- /dev/null +++ b/crates/wasi/src/preview2/host/network.rs @@ -0,0 +1,185 @@ +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 => { + panic!("transient errors should be skipped") + } + + // 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 => ErrorCode::Unknown, + + // Errors that don't correspond to a Rust `io::ErrorKind`. + io::ErrorKind::Other => match error.raw_os_error() { + None => ErrorCode::Unknown, + 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(_) => panic!("unknown error {:?}", error), + }, + _ => panic!("unknown error {:?}", error), + } + .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..439072b1d6dd --- /dev/null +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -0,0 +1,575 @@ +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, HostTcpSocketInner, HostTcpState, TableTcpSocketExt}; +use crate::preview2::{HostPollable, PollableFuture, WasiView}; +use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; +use io_lifetimes::AsSocketlike; +use rustix::net::sockopt; +use std::any::Any; +use std::mem; +use std::pin::Pin; +use std::sync::Arc; +#[cfg(unix)] +use tokio::task::spawn; +#[cfg(not(unix))] +use tokio::task::spawn_blocking; +use tokio::task::JoinHandle; + +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(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &*tcp_state { + HostTcpState::Default => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let network = table.get_network(network)?; + let binder = network.0.tcp_binder(local_address)?; + + binder.bind_existing_tcp_listener(socket.tcp_socket())?; + + *tcp_state = HostTcpState::BindStarted; + socket.inner.sender.send(()).unwrap(); + + Ok(()) + } + + // TODO: Bind and listen aren't really blocking operations; figure this + // out at the spec level. + fn finish_bind(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &mut *tcp_state { + HostTcpState::BindStarted => { + *tcp_state = HostTcpState::Bound; + Ok(()) + } + _ => Err(ErrorCode::NotInProgress.into()), + } + } + + fn start_connect( + &mut self, + this: tcp::TcpSocket, + network: Network, + remote_address: IpSocketAddress, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &*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 a host `connect`. Our socket is non-blocking, so it'll either... + match connecter.connect_existing_tcp_listener(socket.tcp_socket()) { + // succeed immediately, + Ok(()) => { + *tcp_state = HostTcpState::ConnectReady(Ok(())); + return Ok(()); + } + // continue in progress, + Err(err) + if err.raw_os_error() == Some(rustix::io::Errno::INPROGRESS.raw_os_error()) => {} + // or fail immediately. + Err(err) => return Err(err.into()), + } + + // The connect is continuing in progres. Set up the join handle. + + let clone = socket.clone_inner(); + + #[cfg(unix)] + let join = spawn(async move { + let result = match clone.tcp_socket.writable().await { + Ok(mut writable) => { + writable.retain_ready(); + + // Check whether the connect succeeded. + match sockopt::get_socket_error(&clone.tcp_socket) { + Ok(Ok(())) => Ok(()), + Err(err) | Ok(Err(err)) => Err(err.into()), + } + } + Err(err) => Err(err), + }; + + *clone.tcp_state.write().unwrap() = HostTcpState::ConnectReady(result); + clone.sender.send(()).unwrap(); + }); + + #[cfg(not(unix))] + let join = spawn_blocking(move || { + let result = match rustix::event::poll( + &mut [rustix::event::PollFd::new( + &clone.tcp_socket, + rustix::event::PollFlags::OUT, + )], + -1, + ) { + Ok(_) => { + // Check whether the connect succeeded. + match sockopt::get_socket_error(&clone.tcp_socket) { + Ok(Ok(())) => Ok(()), + Err(err) | Ok(Err(err)) => Err(err.into()), + } + } + Err(err) => Err(err.into()), + }; + + *clone.tcp_state.write().unwrap() = HostTcpState::ConnectReady(result); + clone.sender.send(()).unwrap(); + }); + + *tcp_state = HostTcpState::Connecting(Pin::from(Box::new(join))); + + Ok(()) + } + + fn finish_connect( + &mut self, + this: tcp::TcpSocket, + ) -> Result<(InputStream, OutputStream), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &mut *tcp_state { + HostTcpState::ConnectReady(_) => {} + HostTcpState::Connecting(join) => match maybe_unwrap_future(join) { + Some(joined) => joined.unwrap(), + None => return Err(ErrorCode::WouldBlock.into()), + }, + _ => return Err(ErrorCode::NotInProgress.into()), + }; + + let old_state = mem::replace(&mut *tcp_state, HostTcpState::Connected); + + // Extract the connection result. + let result = match old_state { + HostTcpState::ConnectReady(result) => result, + _ => panic!(), + }; + + // Report errors, resetting the state if needed. + match result { + Ok(()) => {} + Err(err) => { + *tcp_state = HostTcpState::Default; + return Err(err.into()); + } + } + + drop(tcp_state); + + 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, + _network: Network, + ) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &*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)?; + + *tcp_state = HostTcpState::ListenStarted; + socket.inner.sender.send(()).unwrap(); + + Ok(()) + } + + fn finish_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { + let table = self.table(); + let socket = table.get_tcp_socket(this)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + + match &mut *tcp_state { + HostTcpState::ListenStarted => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); + *tcp_state = HostTcpState::Listening(Pin::from(Box::new(new_join))); + drop(tcp_state); + + 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)?; + + let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + match &mut *tcp_state { + HostTcpState::ListenReady(_) => {} + HostTcpState::Listening(join) => match maybe_unwrap_future(join) { + Some(joined) => joined.unwrap(), + None => return Err(ErrorCode::WouldBlock.into()), + }, + HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); + *tcp_state = HostTcpState::Listening(Pin::from(Box::new(new_join))); + drop(tcp_state); + + // Do the host system 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 + .inner + .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 + .inner + .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(apple, windows, 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(apple, windows, 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)?; + + let tcp_state = socket.inner.tcp_state.read().unwrap(); + match &*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(rustix::io::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(rustix::io::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"); + + Box::pin(async { + socket.receiver.changed().await.unwrap(); + Ok(()) + }) + } + + 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 + .inner + .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)?; + + // On non-Unix platforms, do a `shutdown` to wake up `poll`. + #[cfg(not(unix))] + rustix::net::shutdown(&dropped.inner.tcp_socket, rustix::net::Shutdown::ReadWrite).unwrap(); + + drop(dropped); + + Ok(()) + } +} + +/// Spawn a task to monitor a socket for incoming connections that +/// can be `accept`ed. +fn spawn_task_to_wait_for_connections(socket: Arc) -> JoinHandle<()> { + #[cfg(unix)] + let new_join = spawn(async move { + socket.tcp_socket.readable().await.unwrap().retain_ready(); + *socket.tcp_state.write().unwrap() = HostTcpState::ListenReady(Ok(())); + socket.sender.send(()).unwrap(); + }); + + #[cfg(not(unix))] + let new_join = spawn_blocking(move || { + let result = match rustix::event::poll( + &mut [rustix::event::PollFd::new( + &socket.tcp_socket, + rustix::event::PollFlags::IN, + )], + -1, + ) { + Ok(_) => Ok(()), + Err(err) => Err(err.into()), + }; + *socket.tcp_state.write().unwrap() = HostTcpState::ListenReady(result); + socket.sender.send(()).unwrap(); + }); + + new_join +} + +/// Given a future, return the finished value if it's already ready, or +/// `None` if it's not. +fn maybe_unwrap_future( + future: &mut Pin>, +) -> Option { + use std::ptr; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + + unsafe fn clone(_ptr: *const ()) -> RawWaker { + const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); + RawWaker::new(std::ptr::null(), &VTABLE) + } + unsafe fn wake(_ptr: *const ()) {} + unsafe fn wake_by_ref(_ptr: *const ()) {} + unsafe fn drop(_ptr: *const ()) {} + + let waker = unsafe { Waker::from_raw(clone(ptr::null() as _)) }; + + let mut cx = Context::from_waker(&waker); + match future.as_mut().poll(&mut cx) { + Poll::Ready(val) => Some(val), + Poll::Pending => None, + } +} 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..75e0b3fbdc3f --- /dev/null +++ b/crates/wasi/src/preview2/tcp.rs @@ -0,0 +1,290 @@ +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::pin::Pin; +use std::sync::{Arc, RwLock}; +use system_interface::io::IoExt; +use tokio::sync::watch::{channel, Receiver, Sender}; +use tokio::task::JoinHandle; + +/// 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 incomming connection. + Listening(Pin>>), + + /// Listening heard an incomming connection arrive that is ready to be + /// accepted. + ListenReady(io::Result<()>), + + /// An outgoing connection is started via `start_connect`. + Connecting(Pin>>), + + /// An outgoing connection is ready to be established. + ConnectReady(io::Result<()>), + + /// 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 recieving end of `inner`'s `sender`, used by `subscribe` + /// subscriptions to wait for I/O. + pub(crate) receiver: Receiver<()>, +} + +/// 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, + + /// The current state in the bind/listen/accept/connect progression. + pub(crate) tcp_state: RwLock, + + /// A sender used to send messages when I/O events complete. + pub(crate) sender: Sender<()>, +} + +impl HostTcpSocket { + pub fn new(family: AddressFamily) -> io::Result { + 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)?; + + let (sender, receiver) = channel(()); + + Ok(Self { + inner: Arc::new(HostTcpSocketInner { + tcp_socket, + tcp_state: RwLock::new(HostTcpState::Default), + sender, + }), + receiver, + }) + } + + 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)?; + + let (sender, receiver) = channel(()); + + Ok(Self { + inner: Arc::new(HostTcpSocketInner { + tcp_socket, + tcp_state: RwLock::new(HostTcpState::Default), + sender, + }), + receiver, + }) + } + + 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)> { + 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, + )], + -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)> { + 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, + )], + -1, + ) { + Ok(_) => Ok(()), + Err(err) => Err(err.into()), + } + }) + .await + } + } +} + +impl Drop for HostTcpSocketInner { + fn drop(&mut self) { + match &*self.tcp_state.read().unwrap() { + HostTcpState::Default + | HostTcpState::BindStarted + | HostTcpState::Bound + | HostTcpState::ListenStarted + | HostTcpState::ListenReady(_) + | HostTcpState::ConnectReady(_) + | HostTcpState::Connected => {} + HostTcpState::Listening(join) | HostTcpState::Connecting(join) => { + // Abort the tasks so that they don't detach. + join.abort(); + } + } + } +} + +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>; +} + +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) + } +} + +pub(crate) fn read_result( + r: Result, +) -> Result<(usize, StreamState), std::io::Error> { + match r { + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n, StreamState::Open)), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), + Err(e) => Err(e), + } +} + +pub(crate) fn write_result( + r: Result, +) -> Result<(usize, StreamState), std::io::Error> { + match r { + Ok(0) => Ok((0, StreamState::Closed)), + Ok(n) => Ok((n, StreamState::Open)), + Err(e) => Err(e), + } +} 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/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" From bd3fe3ed222cda04929ed459386afcf41574e5a0 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Wed, 16 Aug 2023 10:49:35 -0700 Subject: [PATCH 02/16] Minor cleanups. --- crates/wasi/src/preview2/tcp.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 75e0b3fbdc3f..59c54919a6dc 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -28,10 +28,10 @@ pub(crate) enum HostTcpState { /// Listening started via `listen_start`. ListenStarted, - /// The socket is now listening and waiting for an incomming connection. + /// The socket is now listening and waiting for an incoming connection. Listening(Pin>>), - /// Listening heard an incomming connection arrive that is ready to be + /// Listening heard an incoming connection arrive that is ready to be /// accepted. ListenReady(io::Result<()>), @@ -46,10 +46,11 @@ pub(crate) enum HostTcpState { } /// 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 +/// +/// 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. @@ -78,7 +79,10 @@ pub(crate) struct HostTcpSocketInner { } 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. @@ -97,6 +101,9 @@ impl HostTcpSocket { }) } + /// 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); @@ -268,20 +275,16 @@ impl TableTcpSocketExt for Table { } } -pub(crate) fn read_result( - r: Result, -) -> Result<(usize, StreamState), std::io::Error> { +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() == std::io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), + Err(e) if e.kind() == io::ErrorKind::Interrupted => Ok((0, StreamState::Open)), Err(e) => Err(e), } } -pub(crate) fn write_result( - r: Result, -) -> Result<(usize, StreamState), std::io::Error> { +pub(crate) fn write_result(r: io::Result) -> io::Result<(usize, StreamState)> { match r { Ok(0) => Ok((0, StreamState::Closed)), Ok(n) => Ok((n, StreamState::Open)), From c6765e549cce78c40b19d9706ba03e59dfd4af83 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Wed, 16 Aug 2023 11:04:24 -0700 Subject: [PATCH 03/16] Update to the latest upstream wasi-sockets. --- .../wasi-sockets-tests/src/bin/tcp_v4.rs | 2 +- .../wasi-sockets-tests/src/bin/tcp_v6.rs | 2 +- crates/wasi/src/preview2/host/tcp.rs | 6 +- .../wasi/wit/deps/sockets/ip-name-lookup.wit | 22 ++--- .../wit/deps/sockets/tcp-create-socket.wit | 10 +- crates/wasi/wit/deps/sockets/tcp.wit | 92 +++++++++--------- .../wit/deps/sockets/udp-create-socket.wit | 10 +- crates/wasi/wit/deps/sockets/udp.wit | 93 +++++++++++-------- 8 files changed, 123 insertions(+), 114 deletions(-) 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 index fff3d6a09093..f0e971ac1252 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs @@ -34,7 +34,7 @@ fn main() { wait(sub); tcp::finish_bind(sock).unwrap(); - tcp::start_listen(sock, net).unwrap(); + tcp::start_listen(sock).unwrap(); wait(sub); tcp::finish_listen(sock).unwrap(); 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 index 64b89957cde4..d3db94ddcef2 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs @@ -36,7 +36,7 @@ fn main() { wait(sub); tcp::finish_bind(sock).unwrap(); - tcp::start_listen(sock, net).unwrap(); + tcp::start_listen(sock).unwrap(); wait(sub); tcp::finish_listen(sock).unwrap(); diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 439072b1d6dd..de3d68ff3011 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -196,11 +196,7 @@ impl tcp::Host for T { Ok((input_stream, output_stream)) } - fn start_listen( - &mut self, - this: tcp::TcpSocket, - _network: Network, - ) -> Result<(), network::Error> { + fn start_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; 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/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..948ed581a378 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,25 @@ 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 +210,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) } From 73cbde9151351ec59aa28926bee96db5ef16abe7 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Wed, 16 Aug 2023 17:07:18 -0700 Subject: [PATCH 04/16] Address review feedback. --- crates/wasi/Cargo.toml | 1 + crates/wasi/src/preview2/host/network.rs | 11 +- crates/wasi/src/preview2/host/tcp.rs | 123 ++++++++++++++--------- crates/wasi/src/preview2/tcp.rs | 29 +++++- 4 files changed, 108 insertions(+), 56 deletions(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 5cd5602d3fda..edd97477af9a 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -51,6 +51,7 @@ libc = { workspace = true } [target.'cfg(windows)'.dependencies] io-extras = { workspace = true } windows-sys = { workspace = true } +rustix = { workspace = true, features = ["net"], optional = true } [features] default = ["sync", "preview2", "preview1-on-preview2"] diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 5f6b090d6802..023a5d9f3026 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -39,7 +39,8 @@ impl From for network::Error { // Errors we don't expect to see here. io::ErrorKind::Interrupted | io::ErrorKind::ConnectionAborted => { - panic!("transient errors should be skipped") + // Transient errors should be skipped. + return Self::trap(error.into()); } // Errors not expected from network APIs. @@ -49,11 +50,11 @@ impl From for network::Error { | io::ErrorKind::BrokenPipe | io::ErrorKind::NotFound | io::ErrorKind::UnexpectedEof - | io::ErrorKind::AlreadyExists => ErrorCode::Unknown, + | 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 => ErrorCode::Unknown, + 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) => { @@ -62,9 +63,9 @@ impl From for network::Error { Some(libc::ECONNRESET) => ErrorCode::ConnectionReset, Some(libc::ECONNREFUSED) => ErrorCode::ConnectionRefused, Some(libc::EADDRINUSE) => ErrorCode::AddressInUse, - Some(_) => panic!("unknown error {:?}", error), + Some(_) => return Self::trap(error.into()), }, - _ => panic!("unknown error {:?}", error), + _ => return Self::trap(error.into()), } .into() } diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index de3d68ff3011..a757a1c143ae 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -16,6 +16,7 @@ use std::any::Any; use std::mem; use std::pin::Pin; use std::sync::Arc; +use std::sync::RwLockWriteGuard; #[cfg(unix)] use tokio::task::spawn; #[cfg(not(unix))] @@ -32,7 +33,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let tcp_state = socket.tcp_state_write_lock(); match &*tcp_state { HostTcpState::Default => {} _ => return Err(ErrorCode::NotInProgress.into()), @@ -43,26 +44,25 @@ impl tcp::Host for T { binder.bind_existing_tcp_listener(socket.tcp_socket())?; - *tcp_state = HostTcpState::BindStarted; - socket.inner.sender.send(()).unwrap(); + set_state(tcp_state, HostTcpState::BindStarted); + socket.notify(); Ok(()) } - // TODO: Bind and listen aren't really blocking operations; figure this - // out at the spec level. fn finish_bind(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); - match &mut *tcp_state { - HostTcpState::BindStarted => { - *tcp_state = HostTcpState::Bound; - Ok(()) - } - _ => Err(ErrorCode::NotInProgress.into()), + let tcp_state = socket.tcp_state_write_lock(); + match &*tcp_state { + HostTcpState::BindStarted => {} + _ => return Err(ErrorCode::NotInProgress.into()), } + + set_state(tcp_state, HostTcpState::Bound); + + Ok(()) } fn start_connect( @@ -74,7 +74,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let tcp_state = socket.tcp_state_write_lock(); match &*tcp_state { HostTcpState::Default => {} HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), @@ -88,7 +88,8 @@ impl tcp::Host for T { match connecter.connect_existing_tcp_listener(socket.tcp_socket()) { // succeed immediately, Ok(()) => { - *tcp_state = HostTcpState::ConnectReady(Ok(())); + set_state(tcp_state, HostTcpState::ConnectReady(Ok(()))); + socket.notify(); return Ok(()); } // continue in progress, @@ -98,7 +99,7 @@ impl tcp::Host for T { Err(err) => return Err(err.into()), } - // The connect is continuing in progres. Set up the join handle. + // The connect is continuing in progress. Set up the join handle. let clone = socket.clone_inner(); @@ -117,8 +118,7 @@ impl tcp::Host for T { Err(err) => Err(err), }; - *clone.tcp_state.write().unwrap() = HostTcpState::ConnectReady(result); - clone.sender.send(()).unwrap(); + clone.set_state_and_notify(HostTcpState::ConnectReady(result)); }); #[cfg(not(unix))] @@ -140,11 +140,13 @@ impl tcp::Host for T { Err(err) => Err(err.into()), }; - *clone.tcp_state.write().unwrap() = HostTcpState::ConnectReady(result); - clone.sender.send(()).unwrap(); + clone.set_state_and_notify(HostTcpState::ConnectReady(result)); }); - *tcp_state = HostTcpState::Connecting(Pin::from(Box::new(join))); + set_state( + tcp_state, + HostTcpState::Connecting(Pin::from(Box::new(join))), + ); Ok(()) } @@ -156,7 +158,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let mut tcp_state = socket.tcp_state_write_lock(); match &mut *tcp_state { HostTcpState::ConnectReady(_) => {} HostTcpState::Connecting(join) => match maybe_unwrap_future(join) { @@ -171,14 +173,14 @@ impl tcp::Host for T { // Extract the connection result. let result = match old_state { HostTcpState::ConnectReady(result) => result, - _ => panic!(), + _ => unreachable!(), }; // Report errors, resetting the state if needed. match result { Ok(()) => {} Err(err) => { - *tcp_state = HostTcpState::Default; + set_state(tcp_state, HostTcpState::Default); return Err(err.into()); } } @@ -200,7 +202,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let tcp_state = socket.tcp_state_write_lock(); match &*tcp_state { HostTcpState::Bound => {} HostTcpState::ListenStarted => return Err(ErrorCode::AlreadyListening.into()), @@ -210,8 +212,8 @@ impl tcp::Host for T { socket.tcp_socket().listen(None)?; - *tcp_state = HostTcpState::ListenStarted; - socket.inner.sender.send(()).unwrap(); + set_state(tcp_state, HostTcpState::ListenStarted); + socket.notify(); Ok(()) } @@ -220,16 +222,18 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let tcp_state = socket.tcp_state_write_lock(); - match &mut *tcp_state { + match &*tcp_state { HostTcpState::ListenStarted => {} _ => return Err(ErrorCode::NotInProgress.into()), } let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); - *tcp_state = HostTcpState::Listening(Pin::from(Box::new(new_join))); - drop(tcp_state); + set_state( + tcp_state, + HostTcpState::Listening(Pin::from(Box::new(new_join))), + ); Ok(()) } @@ -241,7 +245,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.inner.tcp_state.write().unwrap(); + let mut tcp_state = socket.tcp_state_write_lock(); match &mut *tcp_state { HostTcpState::ListenReady(_) => {} HostTcpState::Listening(join) => match maybe_unwrap_future(join) { @@ -253,8 +257,10 @@ impl tcp::Host for T { } let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); - *tcp_state = HostTcpState::Listening(Pin::from(Box::new(new_join))); - drop(tcp_state); + set_state( + tcp_state, + HostTcpState::Listening(Pin::from(Box::new(new_join))), + ); // Do the host system call. let (connection, _addr) = socket.tcp_socket().accept_with(Blocking::No)?; @@ -347,7 +353,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let tcp_state = socket.inner.tcp_state.read().unwrap(); + let tcp_state = socket.tcp_state_read_lock(); match &*tcp_state { HostTcpState::Listening(_) => {} _ => return Err(ErrorCode::NotInProgress.into()), @@ -506,7 +512,8 @@ impl tcp::Host for T { // doesn't block. let dropped = table.delete_tcp_socket(this)?; - // On non-Unix platforms, do a `shutdown` to wake up `poll`. + // On non-Unix platforms, do a `shutdown` to wake up any `poll` calls + // that are waiting. #[cfg(not(unix))] rustix::net::shutdown(&dropped.inner.tcp_socket, rustix::net::Shutdown::ReadWrite).unwrap(); @@ -520,14 +527,13 @@ impl tcp::Host for T { /// can be `accept`ed. fn spawn_task_to_wait_for_connections(socket: Arc) -> JoinHandle<()> { #[cfg(unix)] - let new_join = spawn(async move { + let join = spawn(async move { socket.tcp_socket.readable().await.unwrap().retain_ready(); - *socket.tcp_state.write().unwrap() = HostTcpState::ListenReady(Ok(())); - socket.sender.send(()).unwrap(); + socket.set_state_and_notify(HostTcpState::ListenReady(Ok(()))); }); #[cfg(not(unix))] - let new_join = spawn_blocking(move || { + let join = spawn_blocking(move || { let result = match rustix::event::poll( &mut [rustix::event::PollFd::new( &socket.tcp_socket, @@ -538,11 +544,16 @@ fn spawn_task_to_wait_for_connections(socket: Arc) -> JoinHa Ok(_) => Ok(()), Err(err) => Err(err.into()), }; - *socket.tcp_state.write().unwrap() = HostTcpState::ListenReady(result); - socket.sender.send(()).unwrap(); + socket.set_state_and_notify(HostTcpState::ListenReady(result)); }); - new_join + join +} + +/// Set `*tcp_state` to `new_state` and consume `tcp_state`. +fn set_state(tcp_state: RwLockWriteGuard, new_state: HostTcpState) { + let mut tcp_state = tcp_state; + *tcp_state = new_state; } /// Given a future, return the finished value if it's already ready, or @@ -553,16 +564,28 @@ fn maybe_unwrap_future( use std::ptr; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - unsafe fn clone(_ptr: *const ()) -> RawWaker { - const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop); - RawWaker::new(std::ptr::null(), &VTABLE) + // Create a no-op Waker. This is derived from [code in std] and can + // be replaced with `std::task::Waker::noop()` when the "noop_waker" + // feature is stablized. + // + // [code in std]: https://github.com/rust-lang/rust/blob/27fb598d51d4566a725e4868eaf5d2e15775193e/library/core/src/task/wake.rs#L349 + fn noop_waker() -> Waker { + const VTABLE: RawWakerVTable = RawWakerVTable::new( + // Cloning just returns a new no-op raw waker + |_| RAW, + // `wake` does nothing + |_| {}, + // `wake_by_ref` does nothing + |_| {}, + // Dropping does nothing as we don't allocate anything + |_| {}, + ); + const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE); + + unsafe { Waker::from_raw(RAW) } } - unsafe fn wake(_ptr: *const ()) {} - unsafe fn wake_by_ref(_ptr: *const ()) {} - unsafe fn drop(_ptr: *const ()) {} - - let waker = unsafe { Waker::from_raw(clone(ptr::null() as _)) }; + let waker = noop_waker(); let mut cx = Context::from_waker(&waker); match future.as_mut().poll(&mut cx) { Poll::Ready(val) => Some(val), diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 59c54919a6dc..92c812bbea28 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -5,7 +5,7 @@ use cap_std::net::{TcpListener, TcpStream}; use io_lifetimes::AsSocketlike; use std::io; use std::pin::Pin; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use system_interface::io::IoExt; use tokio::sync::watch::{channel, Receiver, Sender}; use tokio::task::JoinHandle; @@ -128,9 +128,23 @@ impl HostTcpSocket { self.inner.tcp_socket() } + pub fn notify(&self) { + self.inner.notify() + } + pub fn clone_inner(&self) -> Arc { Arc::clone(&self.inner) } + + /// Acquire a reader lock for `self.tcp_state`. + pub fn tcp_state_read_lock(&self) -> RwLockReadGuard { + self.inner.tcp_state.read().unwrap() + } + + /// Acquire a writer lock for `self.tcp_state`. + pub fn tcp_state_write_lock(&self) -> RwLockWriteGuard { + self.inner.tcp_state.write().unwrap() + } } impl HostTcpSocketInner { @@ -144,6 +158,19 @@ impl HostTcpSocketInner { tcp_socket } + pub fn notify(&self) { + self.sender.send(()).unwrap() + } + + pub fn set_state(&self, new_state: HostTcpState) { + *self.tcp_state.write().unwrap() = new_state; + } + + pub fn set_state_and_notify(&self, new_state: HostTcpState) { + self.set_state(new_state); + self.notify() + } + /// Spawn a task on tokio's blocking thread for performing blocking /// syscalls on the underlying [`cap_std::net::TcpListener`]. #[cfg(not(unix))] From 58b5b085e8b5998df3a0f787d1aaab74a6a5d15d Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 17 Aug 2023 08:40:48 -0700 Subject: [PATCH 05/16] Handle zero-length reads and writes, and other cleanups. --- .../wasi-sockets-tests/src/bin/tcp_v4.rs | 21 +++++++++++++++---- .../wasi-sockets-tests/src/bin/tcp_v6.rs | 21 +++++++++++++++---- crates/wasi/src/preview2/host/tcp.rs | 14 ++++++------- crates/wasi/src/preview2/tcp.rs | 13 ++++++++++++ 4 files changed, 53 insertions(+), 16 deletions(-) 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 index f0e971ac1252..907b8986b7de 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs @@ -47,8 +47,13 @@ fn main() { wait(client_sub); let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - let (n, _status) = streams::write(client_output, first_message).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); @@ -57,7 +62,13 @@ fn main() { wait(sub); let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, _status) = streams::read(input, first_message.len() as u64).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); @@ -74,8 +85,9 @@ fn main() { wait(client_sub); let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - let (n, _status) = streams::write(client_output, second_message).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); @@ -84,7 +96,8 @@ fn main() { wait(sub); let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, _status) = streams::read(input, second_message.len() as u64).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); 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 index d3db94ddcef2..47a569aed30c 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs @@ -49,8 +49,13 @@ fn main() { wait(client_sub); let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - let (n, _status) = streams::write(client_output, first_message).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); @@ -59,7 +64,13 @@ fn main() { wait(sub); let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, _status) = streams::read(input, first_message.len() as u64).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); @@ -76,8 +87,9 @@ fn main() { wait(client_sub); let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - let (n, _status) = streams::write(client_output, second_message).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); @@ -86,7 +98,8 @@ fn main() { wait(sub); let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, _status) = streams::read(input, second_message.len() as u64).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); diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index a757a1c143ae..8d65663eabf3 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -42,6 +42,7 @@ impl tcp::Host for T { 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())?; set_state(tcp_state, HostTcpState::BindStarted); @@ -84,7 +85,7 @@ impl tcp::Host for T { let network = table.get_network(network)?; let connecter = network.0.tcp_connecter(remote_address)?; - // Do a host `connect`. Our socket is non-blocking, so it'll either... + // 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(()) => { @@ -262,7 +263,7 @@ impl tcp::Host for T { HostTcpState::Listening(Pin::from(Box::new(new_join))), ); - // Do the host system call. + // Do the OS accept call. let (connection, _addr) = socket.tcp_socket().accept_with(Blocking::No)?; let tcp_socket = HostTcpSocket::from_tcp_stream(connection)?; @@ -282,8 +283,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; let addr = socket - .inner - .tcp_socket + .tcp_socket() .as_socketlike_view::() .local_addr()?; Ok(addr.into()) @@ -293,8 +293,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; let addr = socket - .inner - .tcp_socket + .tcp_socket() .as_socketlike_view::() .peer_addr()?; Ok(addr.into()) @@ -498,8 +497,7 @@ impl tcp::Host for T { }; socket - .inner - .tcp_socket + .tcp_socket() .as_socketlike_view::() .shutdown(how)?; Ok(()) diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 92c812bbea28..f4cfc80ce479 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -189,6 +189,9 @@ impl HostTcpSocketInner { #[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() @@ -228,6 +231,9 @@ impl HostInputStream for Arc { #[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::() @@ -313,8 +319,15 @@ pub(crate) fn read_result(r: io::Result) -> io::Result<(usize, StreamStat 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), } } From 7c11589cb32b036749d0858e1563ab16483a91e7 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 17 Aug 2023 18:46:29 -0700 Subject: [PATCH 06/16] Fix compilation on macOS. --- crates/wasi/src/preview2/host/tcp.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 8d65663eabf3..e2da82952148 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -306,7 +306,13 @@ impl tcp::Host for T { // If `SO_DOMAIN` is available, use it. // // TODO: OpenBSD also supports this; upstream PRs are posted. - #[cfg(not(any(apple, windows, target_os = "netbsd", target_os = "openbsd")))] + #[cfg(not(any( + windows, + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] { use rustix::net::AddressFamily; @@ -320,7 +326,13 @@ impl tcp::Host for T { } // When `SO_DOMAIN` is not available, emulate it. - #[cfg(any(apple, windows, target_os = "netbsd", target_os = "openbsd"))] + #[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); From eca3cc61e4debc3e0c1e799fc652618f6d93915d Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 17 Aug 2023 19:50:56 -0700 Subject: [PATCH 07/16] Fix compilation on Windows. --- crates/wasi/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index edd97477af9a..a67fd1de510e 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -51,7 +51,7 @@ libc = { workspace = true } [target.'cfg(windows)'.dependencies] io-extras = { workspace = true } windows-sys = { workspace = true } -rustix = { workspace = true, features = ["net"], optional = true } +rustix = { workspace = true, features = ["event", "net"], optional = true } [features] default = ["sync", "preview2", "preview1-on-preview2"] From 151f80a61c44ab0f159d7d9dfb808b4fb6287b12 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 17 Aug 2023 21:22:55 -0700 Subject: [PATCH 08/16] Update all the copies of wasi-socket wit files. --- .../wit/deps/sockets/ip-name-lookup.wit | 22 ++--- crates/wasi-http/wit/deps/sockets/network.wit | 6 +- .../wit/deps/sockets/tcp-create-socket.wit | 10 +- crates/wasi-http/wit/deps/sockets/tcp.wit | 92 +++++++++--------- .../wit/deps/sockets/udp-create-socket.wit | 10 +- crates/wasi-http/wit/deps/sockets/udp.wit | 93 ++++++++++--------- crates/wasi/wit/deps/sockets/network.wit | 6 +- crates/wasi/wit/deps/sockets/udp.wit | 2 - 8 files changed, 125 insertions(+), 116 deletions(-) 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/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/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index 948ed581a378..700b9e247692 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -198,8 +198,6 @@ interface udp { /// 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 From bffaf9d880ed2be3b4477b26853a4c991935af2a Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 17 Aug 2023 21:25:52 -0700 Subject: [PATCH 09/16] Sync up more wit files. --- crates/wasi-http/wit/test.wit | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 +} From 1ca476345d030151c6819d3d4e3d84a3d56ef1e8 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 18 Aug 2023 05:56:52 -0700 Subject: [PATCH 10/16] Fix the errno code for non-blocking `connect` on Windows. prtest:full --- crates/wasi/src/preview2/host/tcp.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index e2da82952148..67fa245a461c 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -11,6 +11,7 @@ use crate::preview2::tcp::{HostTcpSocket, HostTcpSocketInner, HostTcpState, Tabl 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; use std::mem; @@ -94,8 +95,7 @@ impl tcp::Host for T { return Ok(()); } // continue in progress, - Err(err) - if err.raw_os_error() == Some(rustix::io::Errno::INPROGRESS.raw_os_error()) => {} + Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} // or fail immediately. Err(err) => return Err(err.into()), } @@ -406,7 +406,7 @@ impl tcp::Host for T { // fall back to the other. match sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { Ok(value) => Ok(value), - Err(rustix::io::Errno::NOPROTOOPT) => { + Err(Errno::NOPROTOOPT) => { let value = sockopt::get_ip_ttl(socket.tcp_socket())?; let value = value.try_into().unwrap(); Ok(value) @@ -427,9 +427,7 @@ impl tcp::Host for T { // fall back to the other. match sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value)) { Ok(()) => Ok(()), - Err(rustix::io::Errno::NOPROTOOPT) => { - Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?) - } + Err(Errno::NOPROTOOPT) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), Err(err) => Err(err.into()), } } @@ -602,3 +600,13 @@ fn maybe_unwrap_future( Poll::Pending => None, } } + +// 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; From 1c746b1b243bae416deffb0205a9c9124a0a3bcb Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 18 Aug 2023 06:54:47 -0700 Subject: [PATCH 11/16] Tolerate `NOTCONN` errors when cleaning up with `shutdown`. --- crates/wasi/src/preview2/host/tcp.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 67fa245a461c..9b252b236df6 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -520,10 +520,31 @@ impl tcp::Host for T { // doesn't block. let dropped = table.delete_tcp_socket(this)?; - // On non-Unix platforms, do a `shutdown` to wake up any `poll` calls - // that are waiting. + // If we might have an `event::poll` waiting on the socket, wake it up. #[cfg(not(unix))] - rustix::net::shutdown(&dropped.inner.tcp_socket, rustix::net::Shutdown::ReadWrite).unwrap(); + { + let tcp_state = dropped.tcp_state_read_lock(); + match &*tcp_state { + HostTcpState::Default + | HostTcpState::BindStarted + | HostTcpState::Bound + | HostTcpState::ListenStarted + | HostTcpState::ListenReady(_) + | 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); From c2f07c1dd726a5a955964def23d62811f748800b Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 18 Aug 2023 17:54:08 -0700 Subject: [PATCH 12/16] Simplify the polling mechanism. This requires an updated tokio for `Interest::ERROR`. --- Cargo.lock | 15 +- Cargo.toml | 2 +- crates/wasi/Cargo.toml | 2 +- crates/wasi/src/preview2/host/tcp.rs | 250 +++++++++------------------ crates/wasi/src/preview2/tcp.rs | 67 +------ supply-chain/config.toml | 4 +- 6 files changed, 101 insertions(+), 239 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9112f1e1234e..35a741bd11af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1969,9 +1969,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -2506,12 +2506,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.9" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "winapi", + "windows-sys", ] [[package]] @@ -2747,11 +2747,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", diff --git a/Cargo.toml b/Cargo.toml index 8bd17fd13dc8..85253ac2485e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -245,7 +245,7 @@ tempfile = "3.1.0" filecheck = "0.5.0" libc = "0.2.60" file-per-thread-logger = "0.2.0" -tokio = { version = "1.26.0" } +tokio = { version = "1.32.0" } bytes = "1.4" futures = { version = "0.3.27", default-features = false } indexmap = "2.0.0" diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index a67fd1de510e..6817470e9eda 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -43,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", "net"], optional = true } +rustix = { workspace = true, features = ["event", "fs", "net"], optional = true } [target.'cfg(unix)'.dev-dependencies] libc = { workspace = true } diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 9b252b236df6..91d526e48aeb 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -7,22 +7,18 @@ use crate::preview2::bindings::{ use crate::preview2::network::TableNetworkExt; use crate::preview2::poll::TablePollableExt; use crate::preview2::stream::TableStreamExt; -use crate::preview2::tcp::{HostTcpSocket, HostTcpSocketInner, HostTcpState, TableTcpSocketExt}; +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; -use std::mem; -use std::pin::Pin; -use std::sync::Arc; use std::sync::RwLockWriteGuard; #[cfg(unix)] -use tokio::task::spawn; +use tokio::io::Interest; #[cfg(not(unix))] use tokio::task::spawn_blocking; -use tokio::task::JoinHandle; impl tcp::Host for T { fn start_bind( @@ -47,7 +43,6 @@ impl tcp::Host for T { binder.bind_existing_tcp_listener(socket.tcp_socket())?; set_state(tcp_state, HostTcpState::BindStarted); - socket.notify(); Ok(()) } @@ -90,8 +85,7 @@ impl tcp::Host for T { match connecter.connect_existing_tcp_listener(socket.tcp_socket()) { // succeed immediately, Ok(()) => { - set_state(tcp_state, HostTcpState::ConnectReady(Ok(()))); - socket.notify(); + set_state(tcp_state, HostTcpState::ConnectReady); return Ok(()); } // continue in progress, @@ -100,54 +94,7 @@ impl tcp::Host for T { Err(err) => return Err(err.into()), } - // The connect is continuing in progress. Set up the join handle. - - let clone = socket.clone_inner(); - - #[cfg(unix)] - let join = spawn(async move { - let result = match clone.tcp_socket.writable().await { - Ok(mut writable) => { - writable.retain_ready(); - - // Check whether the connect succeeded. - match sockopt::get_socket_error(&clone.tcp_socket) { - Ok(Ok(())) => Ok(()), - Err(err) | Ok(Err(err)) => Err(err.into()), - } - } - Err(err) => Err(err), - }; - - clone.set_state_and_notify(HostTcpState::ConnectReady(result)); - }); - - #[cfg(not(unix))] - let join = spawn_blocking(move || { - let result = match rustix::event::poll( - &mut [rustix::event::PollFd::new( - &clone.tcp_socket, - rustix::event::PollFlags::OUT, - )], - -1, - ) { - Ok(_) => { - // Check whether the connect succeeded. - match sockopt::get_socket_error(&clone.tcp_socket) { - Ok(Ok(())) => Ok(()), - Err(err) | Ok(Err(err)) => Err(err.into()), - } - } - Err(err) => Err(err.into()), - }; - - clone.set_state_and_notify(HostTcpState::ConnectReady(result)); - }); - - set_state( - tcp_state, - HostTcpState::Connecting(Pin::from(Box::new(join))), - ); + set_state(tcp_state, HostTcpState::Connecting); Ok(()) } @@ -161,32 +108,32 @@ impl tcp::Host for T { let mut tcp_state = socket.tcp_state_write_lock(); match &mut *tcp_state { - HostTcpState::ConnectReady(_) => {} - HostTcpState::Connecting(join) => match maybe_unwrap_future(join) { - Some(joined) => joined.unwrap(), - None => return Err(ErrorCode::WouldBlock.into()), - }, - _ => return Err(ErrorCode::NotInProgress.into()), - }; - - let old_state = mem::replace(&mut *tcp_state, HostTcpState::Connected); - - // Extract the connection result. - let result = match old_state { - HostTcpState::ConnectReady(result) => result, - _ => unreachable!(), - }; + 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(), + } - // Report errors, resetting the state if needed. - match result { - Ok(()) => {} - Err(err) => { - set_state(tcp_state, HostTcpState::Default); - return Err(err.into()); + // 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()), + }; - drop(tcp_state); + set_state(tcp_state, HostTcpState::Connected); let input_clone = socket.clone_inner(); let output_clone = socket.clone_inner(); @@ -214,7 +161,6 @@ impl tcp::Host for T { socket.tcp_socket().listen(None)?; set_state(tcp_state, HostTcpState::ListenStarted); - socket.notify(); Ok(()) } @@ -230,11 +176,7 @@ impl tcp::Host for T { _ => return Err(ErrorCode::NotInProgress.into()), } - let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); - set_state( - tcp_state, - HostTcpState::Listening(Pin::from(Box::new(new_join))), - ); + set_state(tcp_state, HostTcpState::Listening); Ok(()) } @@ -248,20 +190,12 @@ impl tcp::Host for T { let mut tcp_state = socket.tcp_state_write_lock(); match &mut *tcp_state { - HostTcpState::ListenReady(_) => {} - HostTcpState::Listening(join) => match maybe_unwrap_future(join) { - Some(joined) => joined.unwrap(), - None => return Err(ErrorCode::WouldBlock.into()), - }, + HostTcpState::Listening => {} HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), _ => return Err(ErrorCode::NotInProgress.into()), } - let new_join = spawn_task_to_wait_for_connections(socket.clone_inner()); - set_state( - tcp_state, - HostTcpState::Listening(Pin::from(Box::new(new_join))), - ); + set_state(tcp_state, HostTcpState::Listening); // Do the OS accept call. let (connection, _addr) = socket.tcp_socket().accept_with(Blocking::No)?; @@ -366,7 +300,7 @@ impl tcp::Host for T { let tcp_state = socket.tcp_state_read_lock(); match &*tcp_state { - HostTcpState::Listening(_) => {} + HostTcpState::Listening => {} _ => return Err(ErrorCode::NotInProgress.into()), } @@ -478,10 +412,55 @@ impl tcp::Host for T { .downcast_mut::() .expect("downcast to HostTcpSocket failed"); - Box::pin(async { - socket.receiver.changed().await.unwrap(); + // Some states are ready immediately. + match *socket.tcp_state_read_lock() { + HostTcpState::BindStarted + | HostTcpState::ListenStarted + | HostTcpState::ConnectReady => return Box::pin(async { Ok(()) }), + _ => {} + } + + #[cfg(unix)] + let join = Box::pin(async move { + socket + .inner + .tcp_socket + .ready(Interest::READABLE | Interest::WRITABLE | Interest::ERROR) + .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 { @@ -529,12 +508,9 @@ impl tcp::Host for T { | HostTcpState::BindStarted | HostTcpState::Bound | HostTcpState::ListenStarted - | HostTcpState::ListenReady(_) - | HostTcpState::ConnectReady(_) => {} + | HostTcpState::ConnectReady => {} - HostTcpState::Listening(_) - | HostTcpState::Connecting(_) - | HostTcpState::Connected => { + HostTcpState::Listening | HostTcpState::Connecting | HostTcpState::Connected => { match rustix::net::shutdown( &dropped.inner.tcp_socket, rustix::net::Shutdown::ReadWrite, @@ -552,76 +528,12 @@ impl tcp::Host for T { } } -/// Spawn a task to monitor a socket for incoming connections that -/// can be `accept`ed. -fn spawn_task_to_wait_for_connections(socket: Arc) -> JoinHandle<()> { - #[cfg(unix)] - let join = spawn(async move { - socket.tcp_socket.readable().await.unwrap().retain_ready(); - socket.set_state_and_notify(HostTcpState::ListenReady(Ok(()))); - }); - - #[cfg(not(unix))] - let join = spawn_blocking(move || { - let result = match rustix::event::poll( - &mut [rustix::event::PollFd::new( - &socket.tcp_socket, - rustix::event::PollFlags::IN, - )], - -1, - ) { - Ok(_) => Ok(()), - Err(err) => Err(err.into()), - }; - socket.set_state_and_notify(HostTcpState::ListenReady(result)); - }); - - join -} - /// Set `*tcp_state` to `new_state` and consume `tcp_state`. fn set_state(tcp_state: RwLockWriteGuard, new_state: HostTcpState) { let mut tcp_state = tcp_state; *tcp_state = new_state; } -/// Given a future, return the finished value if it's already ready, or -/// `None` if it's not. -fn maybe_unwrap_future( - future: &mut Pin>, -) -> Option { - use std::ptr; - use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; - - // Create a no-op Waker. This is derived from [code in std] and can - // be replaced with `std::task::Waker::noop()` when the "noop_waker" - // feature is stablized. - // - // [code in std]: https://github.com/rust-lang/rust/blob/27fb598d51d4566a725e4868eaf5d2e15775193e/library/core/src/task/wake.rs#L349 - fn noop_waker() -> Waker { - const VTABLE: RawWakerVTable = RawWakerVTable::new( - // Cloning just returns a new no-op raw waker - |_| RAW, - // `wake` does nothing - |_| {}, - // `wake_by_ref` does nothing - |_| {}, - // Dropping does nothing as we don't allocate anything - |_| {}, - ); - const RAW: RawWaker = RawWaker::new(ptr::null(), &VTABLE); - - unsafe { Waker::from_raw(RAW) } - } - - let waker = noop_waker(); - let mut cx = Context::from_waker(&waker); - match future.as_mut().poll(&mut cx) { - Poll::Ready(val) => Some(val), - Poll::Pending => None, - } -} - // On POSIX, non-blocking TCP socket `connect` uses `EINPROGRESS`. // #[cfg(not(windows))] diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index f4cfc80ce479..adc815f1842e 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -4,11 +4,8 @@ use cap_net_ext::{AddressFamily, Blocking, TcpListenerExt}; use cap_std::net::{TcpListener, TcpStream}; use io_lifetimes::AsSocketlike; use std::io; -use std::pin::Pin; use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; use system_interface::io::IoExt; -use tokio::sync::watch::{channel, Receiver, Sender}; -use tokio::task::JoinHandle; /// The state of a TCP socket. /// @@ -29,17 +26,13 @@ pub(crate) enum HostTcpState { ListenStarted, /// The socket is now listening and waiting for an incoming connection. - Listening(Pin>>), - - /// Listening heard an incoming connection arrive that is ready to be - /// accepted. - ListenReady(io::Result<()>), + Listening, /// An outgoing connection is started via `start_connect`. - Connecting(Pin>>), + Connecting, /// An outgoing connection is ready to be established. - ConnectReady(io::Result<()>), + ConnectReady, /// An outgoing connection has been established. Connected, @@ -55,10 +48,6 @@ 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 recieving end of `inner`'s `sender`, used by `subscribe` - /// subscriptions to wait for I/O. - pub(crate) receiver: Receiver<()>, } /// The inner reference-counted state of a `HostTcpSocket`. @@ -73,9 +62,6 @@ pub(crate) struct HostTcpSocketInner { /// The current state in the bind/listen/accept/connect progression. pub(crate) tcp_state: RwLock, - - /// A sender used to send messages when I/O events complete. - pub(crate) sender: Sender<()>, } impl HostTcpSocket { @@ -89,15 +75,11 @@ impl HostTcpSocket { #[cfg(unix)] let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; - let (sender, receiver) = channel(()); - Ok(Self { inner: Arc::new(HostTcpSocketInner { tcp_socket, tcp_state: RwLock::new(HostTcpState::Default), - sender, }), - receiver, }) } @@ -112,15 +94,11 @@ impl HostTcpSocket { #[cfg(unix)] let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; - let (sender, receiver) = channel(()); - Ok(Self { inner: Arc::new(HostTcpSocketInner { tcp_socket, tcp_state: RwLock::new(HostTcpState::Default), - sender, }), - receiver, }) } @@ -128,10 +106,6 @@ impl HostTcpSocket { self.inner.tcp_socket() } - pub fn notify(&self) { - self.inner.notify() - } - pub fn clone_inner(&self) -> Arc { Arc::clone(&self.inner) } @@ -158,19 +132,10 @@ impl HostTcpSocketInner { tcp_socket } - pub fn notify(&self) { - self.sender.send(()).unwrap() - } - pub fn set_state(&self, new_state: HostTcpState) { *self.tcp_state.write().unwrap() = new_state; } - pub fn set_state_and_notify(&self, new_state: HostTcpState) { - self.set_state(new_state); - self.notify() - } - /// Spawn a task on tokio's blocking thread for performing blocking /// syscalls on the underlying [`cap_std::net::TcpListener`]. #[cfg(not(unix))] @@ -215,7 +180,9 @@ impl HostInputStream for Arc { match rustix::event::poll( &mut [rustix::event::PollFd::new( tcp_socket, - rustix::event::PollFlags::IN, + rustix::event::PollFlags::IN + | rustix::event::PollFlags::ERR + | rustix::event::PollFlags::HUP, )], -1, ) { @@ -255,7 +222,9 @@ impl HostOutputStream for Arc { match rustix::event::poll( &mut [rustix::event::PollFd::new( tcp_socket, - rustix::event::PollFlags::OUT, + rustix::event::PollFlags::OUT + | rustix::event::PollFlags::ERR + | rustix::event::PollFlags::HUP, )], -1, ) { @@ -268,24 +237,6 @@ impl HostOutputStream for Arc { } } -impl Drop for HostTcpSocketInner { - fn drop(&mut self) { - match &*self.tcp_state.read().unwrap() { - HostTcpState::Default - | HostTcpState::BindStarted - | HostTcpState::Bound - | HostTcpState::ListenStarted - | HostTcpState::ListenReady(_) - | HostTcpState::ConnectReady(_) - | HostTcpState::Connected => {} - HostTcpState::Listening(join) | HostTcpState::Connecting(join) => { - // Abort the tasks so that they don't detach. - join.abort(); - } - } - } -} - pub(crate) trait TableTcpSocketExt { fn push_tcp_socket(&mut self, tcp_socket: HostTcpSocket) -> Result; fn delete_tcp_socket(&mut self, fd: u32) -> Result; diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 683f4da23a82..b3ee3c5da423 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.5.3" criteria = "safe-to-deploy" [[exemptions.souper-ir]] @@ -589,7 +589,7 @@ version = "1.2.1" criteria = "safe-to-run" [[exemptions.tokio]] -version = "1.29.1" +version = "1.32.0" criteria = "safe-to-deploy" notes = "we are exempting tokio, hyper, and their tightly coupled dependencies by the same authors, expecting that the authors at aws will publish attestions we can import at some point soon" From ebe48cad52aadeca47e3bf4ce9f43a5f791d6fd9 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Tue, 22 Aug 2023 08:01:59 -0700 Subject: [PATCH 13/16] Downgrade to tokio 1.29.1 for now. --- Cargo.lock | 15 ++++++++------- Cargo.toml | 2 +- crates/wasi/src/preview2/host/tcp.rs | 3 ++- supply-chain/config.toml | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 35a741bd11af..9112f1e1234e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1969,9 +1969,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project-lite" -version = "0.2.11" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -2506,12 +2506,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "windows-sys", + "winapi", ] [[package]] @@ -2747,10 +2747,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.32.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ + "autocfg", "backtrace", "bytes", "libc", diff --git a/Cargo.toml b/Cargo.toml index 85253ac2485e..8bd17fd13dc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -245,7 +245,7 @@ tempfile = "3.1.0" filecheck = "0.5.0" libc = "0.2.60" file-per-thread-logger = "0.2.0" -tokio = { version = "1.32.0" } +tokio = { version = "1.26.0" } bytes = "1.4" futures = { version = "0.3.27", default-features = false } indexmap = "2.0.0" diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 91d526e48aeb..51c43283d2ba 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -420,12 +420,13 @@ impl tcp::Host for T { _ => {} } + // 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 | Interest::ERROR) + .ready(Interest::READABLE | Interest::WRITABLE) .await .unwrap() .retain_ready(); diff --git a/supply-chain/config.toml b/supply-chain/config.toml index b3ee3c5da423..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.5.3" +version = "0.4.9" criteria = "safe-to-deploy" [[exemptions.souper-ir]] @@ -589,7 +589,7 @@ version = "1.2.1" criteria = "safe-to-run" [[exemptions.tokio]] -version = "1.32.0" +version = "1.29.1" criteria = "safe-to-deploy" notes = "we are exempting tokio, hyper, and their tightly coupled dependencies by the same authors, expecting that the authors at aws will publish attestions we can import at some point soon" From 9aaa507af5cefea7c5f68a7544288397e917f153 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Tue, 22 Aug 2023 13:04:18 -0700 Subject: [PATCH 14/16] Move `tcp_state` out of the `Arc`. --- crates/wasi/src/preview2/tcp.rs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index adc815f1842e..47b0efefdffb 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -48,6 +48,9 @@ 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: RwLock, } /// The inner reference-counted state of a `HostTcpSocket`. @@ -59,9 +62,6 @@ pub(crate) struct HostTcpSocketInner { /// On non-Unix, we can use plain `poll`. #[cfg(not(unix))] pub(crate) tcp_socket: cap_std::net::TcpListener, - - /// The current state in the bind/listen/accept/connect progression. - pub(crate) tcp_state: RwLock, } impl HostTcpSocket { @@ -76,10 +76,8 @@ impl HostTcpSocket { let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; Ok(Self { - inner: Arc::new(HostTcpSocketInner { - tcp_socket, - tcp_state: RwLock::new(HostTcpState::Default), - }), + inner: Arc::new(HostTcpSocketInner { tcp_socket }), + tcp_state: RwLock::new(HostTcpState::Default), }) } @@ -95,10 +93,8 @@ impl HostTcpSocket { let tcp_socket = tokio::io::unix::AsyncFd::new(tcp_socket)?; Ok(Self { - inner: Arc::new(HostTcpSocketInner { - tcp_socket, - tcp_state: RwLock::new(HostTcpState::Default), - }), + inner: Arc::new(HostTcpSocketInner { tcp_socket }), + tcp_state: RwLock::new(HostTcpState::Default), }) } @@ -112,12 +108,12 @@ impl HostTcpSocket { /// Acquire a reader lock for `self.tcp_state`. pub fn tcp_state_read_lock(&self) -> RwLockReadGuard { - self.inner.tcp_state.read().unwrap() + self.tcp_state.read().unwrap() } /// Acquire a writer lock for `self.tcp_state`. pub fn tcp_state_write_lock(&self) -> RwLockWriteGuard { - self.inner.tcp_state.write().unwrap() + self.tcp_state.write().unwrap() } } @@ -132,10 +128,6 @@ impl HostTcpSocketInner { tcp_socket } - pub fn set_state(&self, new_state: HostTcpState) { - *self.tcp_state.write().unwrap() = new_state; - } - /// Spawn a task on tokio's blocking thread for performing blocking /// syscalls on the underlying [`cap_std::net::TcpListener`]. #[cfg(not(unix))] From 4efaaa97b4c273e4b41632a70db6ff43fa4972c6 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Tue, 22 Aug 2023 13:45:07 -0700 Subject: [PATCH 15/16] `accept` doesn't need a write lock. --- crates/wasi/src/preview2/host/tcp.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 51c43283d2ba..c6ba7d860ab7 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -188,15 +188,12 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let mut tcp_state = socket.tcp_state_write_lock(); - match &mut *tcp_state { + match *socket.tcp_state_read_lock() { HostTcpState::Listening => {} HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), _ => return Err(ErrorCode::NotInProgress.into()), } - set_state(tcp_state, HostTcpState::Listening); - // Do the OS accept call. let (connection, _addr) = socket.tcp_socket().accept_with(Blocking::No)?; let tcp_socket = HostTcpSocket::from_tcp_stream(connection)?; From c696aa8e692f270806902a9f0ea64e3f30d45250 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Tue, 22 Aug 2023 13:48:09 -0700 Subject: [PATCH 16/16] Remove `tcp_state`'s `RwLock`. --- crates/wasi/src/preview2/host/tcp.rs | 73 ++++++++++++---------------- crates/wasi/src/preview2/tcp.rs | 22 +++------ 2 files changed, 38 insertions(+), 57 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index c6ba7d860ab7..a00cc2f863af 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -14,7 +14,6 @@ use io_lifetimes::AsSocketlike; use rustix::io::Errno; use rustix::net::sockopt; use std::any::Any; -use std::sync::RwLockWriteGuard; #[cfg(unix)] use tokio::io::Interest; #[cfg(not(unix))] @@ -27,11 +26,10 @@ impl tcp::Host for T { network: Network, local_address: IpSocketAddress, ) -> Result<(), network::Error> { - let table = self.table(); + let table = self.table_mut(); let socket = table.get_tcp_socket(this)?; - let tcp_state = socket.tcp_state_write_lock(); - match &*tcp_state { + match socket.tcp_state { HostTcpState::Default => {} _ => return Err(ErrorCode::NotInProgress.into()), } @@ -42,22 +40,22 @@ impl tcp::Host for T { // Perform the OS bind call. binder.bind_existing_tcp_listener(socket.tcp_socket())?; - set_state(tcp_state, HostTcpState::BindStarted); + 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(); - let socket = table.get_tcp_socket(this)?; + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; - let tcp_state = socket.tcp_state_write_lock(); - match &*tcp_state { + match socket.tcp_state { HostTcpState::BindStarted => {} _ => return Err(ErrorCode::NotInProgress.into()), } - set_state(tcp_state, HostTcpState::Bound); + socket.tcp_state = HostTcpState::Bound; Ok(()) } @@ -68,11 +66,10 @@ impl tcp::Host for T { network: Network, remote_address: IpSocketAddress, ) -> Result<(), network::Error> { - let table = self.table(); + let table = self.table_mut(); let socket = table.get_tcp_socket(this)?; - let tcp_state = socket.tcp_state_write_lock(); - match &*tcp_state { + match socket.tcp_state { HostTcpState::Default => {} HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), _ => return Err(ErrorCode::NotInProgress.into()), @@ -85,7 +82,8 @@ impl tcp::Host for T { match connecter.connect_existing_tcp_listener(socket.tcp_socket()) { // succeed immediately, Ok(()) => { - set_state(tcp_state, HostTcpState::ConnectReady); + let socket = table.get_tcp_socket_mut(this)?; + socket.tcp_state = HostTcpState::ConnectReady; return Ok(()); } // continue in progress, @@ -94,7 +92,8 @@ impl tcp::Host for T { Err(err) => return Err(err.into()), } - set_state(tcp_state, HostTcpState::Connecting); + let socket = table.get_tcp_socket_mut(this)?; + socket.tcp_state = HostTcpState::Connecting; Ok(()) } @@ -103,11 +102,10 @@ impl tcp::Host for T { &mut self, this: tcp::TcpSocket, ) -> Result<(InputStream, OutputStream), network::Error> { - let table = self.table(); - let socket = table.get_tcp_socket(this)?; + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; - let mut tcp_state = socket.tcp_state_write_lock(); - match &mut *tcp_state { + match socket.tcp_state { HostTcpState::ConnectReady => {} HostTcpState::Connecting => { // Do a `poll` to test for completion, using a timeout of zero @@ -133,7 +131,7 @@ impl tcp::Host for T { _ => return Err(ErrorCode::NotInProgress.into()), }; - set_state(tcp_state, HostTcpState::Connected); + socket.tcp_state = HostTcpState::Connected; let input_clone = socket.clone_inner(); let output_clone = socket.clone_inner(); @@ -147,11 +145,10 @@ impl tcp::Host for T { } fn start_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { - let table = self.table(); - let socket = table.get_tcp_socket(this)?; + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; - let tcp_state = socket.tcp_state_write_lock(); - match &*tcp_state { + match socket.tcp_state { HostTcpState::Bound => {} HostTcpState::ListenStarted => return Err(ErrorCode::AlreadyListening.into()), HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), @@ -160,23 +157,21 @@ impl tcp::Host for T { socket.tcp_socket().listen(None)?; - set_state(tcp_state, HostTcpState::ListenStarted); + socket.tcp_state = HostTcpState::ListenStarted; Ok(()) } fn finish_listen(&mut self, this: tcp::TcpSocket) -> Result<(), network::Error> { - let table = self.table(); - let socket = table.get_tcp_socket(this)?; - - let tcp_state = socket.tcp_state_write_lock(); + let table = self.table_mut(); + let socket = table.get_tcp_socket_mut(this)?; - match &*tcp_state { + match socket.tcp_state { HostTcpState::ListenStarted => {} _ => return Err(ErrorCode::NotInProgress.into()), } - set_state(tcp_state, HostTcpState::Listening); + socket.tcp_state = HostTcpState::Listening; Ok(()) } @@ -188,7 +183,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - match *socket.tcp_state_read_lock() { + match socket.tcp_state { HostTcpState::Listening => {} HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), _ => return Err(ErrorCode::NotInProgress.into()), @@ -295,8 +290,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let tcp_state = socket.tcp_state_read_lock(); - match &*tcp_state { + match socket.tcp_state { HostTcpState::Listening => {} _ => return Err(ErrorCode::NotInProgress.into()), } @@ -410,7 +404,7 @@ impl tcp::Host for T { .expect("downcast to HostTcpSocket failed"); // Some states are ready immediately. - match *socket.tcp_state_read_lock() { + match socket.tcp_state { HostTcpState::BindStarted | HostTcpState::ListenStarted | HostTcpState::ConnectReady => return Box::pin(async { Ok(()) }), @@ -500,8 +494,7 @@ impl tcp::Host for T { // If we might have an `event::poll` waiting on the socket, wake it up. #[cfg(not(unix))] { - let tcp_state = dropped.tcp_state_read_lock(); - match &*tcp_state { + match dropped.tcp_state { HostTcpState::Default | HostTcpState::BindStarted | HostTcpState::Bound @@ -526,12 +519,6 @@ impl tcp::Host for T { } } -/// Set `*tcp_state` to `new_state` and consume `tcp_state`. -fn set_state(tcp_state: RwLockWriteGuard, new_state: HostTcpState) { - let mut tcp_state = tcp_state; - *tcp_state = new_state; -} - // On POSIX, non-blocking TCP socket `connect` uses `EINPROGRESS`. // #[cfg(not(windows))] diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 47b0efefdffb..46acafc027f5 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -4,7 +4,7 @@ use cap_net_ext::{AddressFamily, Blocking, TcpListenerExt}; use cap_std::net::{TcpListener, TcpStream}; use io_lifetimes::AsSocketlike; use std::io; -use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use std::sync::Arc; use system_interface::io::IoExt; /// The state of a TCP socket. @@ -50,7 +50,7 @@ pub(crate) struct HostTcpSocket { pub(crate) inner: Arc, /// The current state in the bind/listen/accept/connect progression. - pub(crate) tcp_state: RwLock, + pub(crate) tcp_state: HostTcpState, } /// The inner reference-counted state of a `HostTcpSocket`. @@ -77,7 +77,7 @@ impl HostTcpSocket { Ok(Self { inner: Arc::new(HostTcpSocketInner { tcp_socket }), - tcp_state: RwLock::new(HostTcpState::Default), + tcp_state: HostTcpState::Default, }) } @@ -94,7 +94,7 @@ impl HostTcpSocket { Ok(Self { inner: Arc::new(HostTcpSocketInner { tcp_socket }), - tcp_state: RwLock::new(HostTcpState::Default), + tcp_state: HostTcpState::Default, }) } @@ -105,16 +105,6 @@ impl HostTcpSocket { pub fn clone_inner(&self) -> Arc { Arc::clone(&self.inner) } - - /// Acquire a reader lock for `self.tcp_state`. - pub fn tcp_state_read_lock(&self) -> RwLockReadGuard { - self.tcp_state.read().unwrap() - } - - /// Acquire a writer lock for `self.tcp_state`. - pub fn tcp_state_write_lock(&self) -> RwLockWriteGuard { - self.tcp_state.write().unwrap() - } } impl HostTcpSocketInner { @@ -234,6 +224,7 @@ pub(crate) trait TableTcpSocketExt { 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 { @@ -249,6 +240,9 @@ impl TableTcpSocketExt for Table { 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)> {