From 405c43668246d5e6c389b078e884e88518e83e98 Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Wed, 15 Apr 2026 08:10:57 +0000 Subject: [PATCH 01/13] docs: add issue 6403 tcp bind plan --- docs/dev/issue-6403-tcp-bind-plan.md | 129 +++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/dev/issue-6403-tcp-bind-plan.md diff --git a/docs/dev/issue-6403-tcp-bind-plan.md b/docs/dev/issue-6403-tcp-bind-plan.md new file mode 100644 index 000000000000..a2499af2061a --- /dev/null +++ b/docs/dev/issue-6403-tcp-bind-plan.md @@ -0,0 +1,129 @@ +# Issue #6403 Plan: Allocate TCP Ephemeral Ports at `bind()` + +Issue: [wasmerio/wasmer#6403](https://github.com/wasmerio/wasmer/issues/6403) + +## Problem Summary + +For TCP sockets in Wasix, `bind(("host", 0))` currently stores the requested address but does not perform a real backend bind. As a result: + +- `getsockname()` after `bind()` still reports port `0` +- the kernel-assigned ephemeral port only appears after `listen()` +- code that relies on POSIX behavior breaks + +Native systems allocate the ephemeral port at `bind()` time, not at `listen()` time. + +## Root Cause + +The relevant behavior is in `lib/wasix/src/net/socket.rs`. + +- `InodeSocket::bind()` stores the requested TCP address for `PreSocket` / `RemoteSocket` stream sockets and returns without calling into the networking backend. +- `InodeSocket::addr_local()` for those pre-listen socket states simply returns the stored address, which remains `host:0`. +- `InodeSocket::listen()` is the first place that actually calls `net.listen_tcp(...)`, so the real OS bind and ephemeral port allocation happen too late. + +This is not just a `getsockname()` reporting bug. The underlying port is not actually reserved until `listen()`. + +## Secondary Constraint + +The current `virtual-net` abstraction exposes: + +- `listen_tcp(...)` +- `bind_udp(...)` +- `connect_tcp(...)` + +but it does not expose a TCP bind primitive that can: + +- perform a real bind without listening yet +- report the effective local address after binding +- later transition into `listen()` or `connect()` + +That means the fix needs to extend the backend abstraction rather than only patching Wasix-local state. + +## Proposed Fix + +### 1. Add a regression test first + +Add a new socket test under `lib/wasix/tests/wasm_tests/socket_tests/` that: + +1. creates an IPv4 TCP socket +2. binds to `127.0.0.1:0` +3. checks that `getsockname().port != 0` immediately after `bind()` +4. calls `listen()` +5. checks that the port stays the same after `listen()` + +This locks in the POSIX behavior expected by the issue report. + +## 2. Introduce a real TCP-bound socket state in `virtual-net` + +Extend `lib/virtual-net` with a TCP bind API and a corresponding bound-socket type that can: + +- return `addr_local()` +- transition into a TCP listener +- transition into a TCP stream connection + +At a minimum, the new backend capability needs to preserve the actual local port selected during `bind()`. + +## 3. Implement the new backend path + +### Host backend + +Update `lib/virtual-net/src/host.rs` to create a TCP socket explicitly, apply socket options, call `bind()`, and read back the effective local address before any later `listen()` or `connect()` step. + +This likely requires `socket2`, similar to the existing UDP bind implementation. + +### Loopback backend + +Update `lib/virtual-net/src/loopback.rs` so a TCP bind to port `0` allocates an ephemeral port during bind, rather than preserving `0` until listen. + +### Remote client/server backend + +Update `lib/virtual-net/src/meta.rs`, `client.rs`, and `server.rs` to carry the new TCP bind operation across the remote networking protocol. + +Without this, Wasix behavior will diverge depending on which backend is active. + +## 4. Update the Wasix socket state machine + +In `lib/wasix/src/net/socket.rs`: + +- make TCP `bind()` return a real upgraded socket object instead of `Ok(None)` +- add a socket state representing “TCP socket bound locally but not yet listening/connected” +- make `addr_local()` read the effective address from that bound socket state +- make `listen()` consume the bound socket instead of rebinding from scratch +- make `connect()` also honor the previously bound local address + +This keeps bind/listen/connect semantics aligned and avoids reporting a port that is not actually reserved. + +## 5. Fix journaling semantics + +`lib/wasix/src/syscalls/wasix/sock_bind.rs` currently journals the requested address from guest memory, which is wrong for `bind(port=0)`. + +After the functional fix: + +- query the effective local address after `sock_bind` succeeds +- journal that effective address instead of the requested `host:0` + +Otherwise journal replay can observe a different port from the one the program originally saw. + +## Implementation Order + +1. Add the Wasix regression test for `bind(..., 0)` + `getsockname()`. +2. Add the new TCP bind abstraction in `virtual-net`. +3. Implement the host backend first. +4. Update Wasix socket state transitions to use the real bound socket. +5. Update journaling to store the effective address. +6. Extend loopback and remote client/server backends. +7. Run targeted socket tests and any relevant `virtual-net` tests. + +## Non-Goals + +- Faking `getsockname()` by inventing a port in Wasix state without actually reserving it +- Fixing only the listen path while leaving bind-then-connect semantics inconsistent +- Fixing only the host backend and leaving other `virtual-net` backends with different behavior + +## Expected Outcome + +After the fix: + +- `bind(("127.0.0.1", 0))` allocates a real ephemeral port immediately +- `getsockname()` reports the assigned port right after `bind()` +- `listen()` keeps the same local port +- journal replay preserves the same observed bound address From d53083f73d60cbbc8ff5cf6c7b7c37572840b019 Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Wed, 15 Apr 2026 12:06:36 +0000 Subject: [PATCH 02/13] fix: allocate tcp ephemeral port at bind time --- lib/virtual-net/src/client.rs | 105 ++++++++++++ lib/virtual-net/src/host.rs | 147 +++++++++++++++-- lib/virtual-net/src/lib.rs | 29 ++++ lib/virtual-net/src/loopback.rs | 118 +++++++++++-- lib/virtual-net/src/meta.rs | 12 ++ lib/virtual-net/src/server.rs | 87 +++++++++- lib/virtual-net/src/tests.rs | 113 +++++++++++++ lib/wasix/src/net/socket.rs | 155 ++++++++++++++++-- lib/wasix/src/syscalls/wasix/sock_bind.rs | 8 +- lib/wasix/tests/wasm_tests/socket_tests.rs | 32 ++++ .../bind-port-zero-connect/build.sh | 3 + .../bind-port-zero-connect/main.c | 119 ++++++++++++++ .../socket_tests/bind-port-zero/build.sh | 3 + .../socket_tests/bind-port-zero/main.c | 75 +++++++++ 14 files changed, 964 insertions(+), 42 deletions(-) create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/build.sh create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/build.sh create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/main.c diff --git a/lib/virtual-net/src/client.rs b/lib/virtual-net/src/client.rs index 52d66ac9fc74..e84854b3e2dc 100644 --- a/lib/virtual-net/src/client.rs +++ b/lib/virtual-net/src/client.rs @@ -54,6 +54,7 @@ use crate::VirtualIoSource; use crate::VirtualNetworking; use crate::VirtualRawSocket; use crate::VirtualSocket; +use crate::VirtualTcpBoundSocket; use crate::VirtualTcpListener; use crate::VirtualTcpSocket; use crate::VirtualUdpSocket; @@ -276,6 +277,7 @@ impl RemoteNetworkingClient { buffer_accept: Default::default(), buffer_recv_with_addr: Default::default(), send_available: 0, + owns_socket_bindings: true, } } } @@ -760,6 +762,39 @@ impl VirtualNetworking for RemoteNetworkingClient { } } + async fn bind_tcp( + &self, + addr: SocketAddr, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, + ) -> Result> { + let socket_id: SocketId = self + .common + .socket_seed + .fetch_add(1, Ordering::SeqCst) + .into(); + match self + .common + .io_iface(RequestType::BindTcp { + socket_id, + addr, + only_v6, + reuse_port, + reuse_addr, + }) + .await + { + ResponseType::Err(err) => Err(err), + ResponseType::None => Ok(Box::new(self.new_socket(socket_id))), + ResponseType::Socket(socket_id) => Ok(Box::new(self.new_socket(socket_id))), + res => { + tracing::debug!("invalid response to bind TCP request - {res:?}"); + Err(NetworkError::IOError) + } + } + } + async fn bind_udp( &self, addr: SocketAddr, @@ -880,9 +915,13 @@ struct RemoteSocket { buffer_recv_with_addr: VecDeque, buffer_accept: VecDeque, send_available: u64, + owns_socket_bindings: bool, } impl Drop for RemoteSocket { fn drop(&mut self) { + if !self.owns_socket_bindings { + return; + } self.common.recv_tx.lock().unwrap().remove(&self.socket_id); self.common .recv_with_addr_tx @@ -941,6 +980,31 @@ impl RemoteSocket { self.pending_accept.replace((child_id, rx_recv)); Ok(()) } + + fn transition_socket(&mut self) -> RemoteSocket { + let (_tx_recv, rx_recv) = tokio::sync::mpsc::channel(1); + let (_tx_recv_with_addr, rx_recv_with_addr) = tokio::sync::mpsc::channel(1); + let (_tx_accept, rx_accept) = tokio::sync::mpsc::channel(1); + let (_tx_sent, rx_sent) = tokio::sync::mpsc::channel(1); + + self.owns_socket_bindings = false; + + RemoteSocket { + socket_id: self.socket_id, + common: self.common.clone(), + rx_buffer: std::mem::take(&mut self.rx_buffer), + rx_recv: std::mem::replace(&mut self.rx_recv, rx_recv), + rx_recv_with_addr: std::mem::replace(&mut self.rx_recv_with_addr, rx_recv_with_addr), + tx_waker: self.tx_waker.clone(), + rx_accept: std::mem::replace(&mut self.rx_accept, rx_accept), + rx_sent: std::mem::replace(&mut self.rx_sent, rx_sent), + pending_accept: self.pending_accept.take(), + buffer_recv_with_addr: std::mem::take(&mut self.buffer_recv_with_addr), + buffer_accept: std::mem::take(&mut self.buffer_accept), + send_available: self.send_available, + owns_socket_bindings: true, + } + } } impl VirtualIoSource for RemoteSocket { @@ -1121,6 +1185,7 @@ impl VirtualTcpListener for RemoteSocket { buffer_accept: Default::default(), buffer_recv_with_addr: Default::default(), send_available: 0, + owns_socket_bindings: true, }; Ok((Box::new(socket), accepted.addr)) } @@ -1159,6 +1224,46 @@ impl VirtualTcpListener for RemoteSocket { } } +impl VirtualTcpBoundSocket for RemoteSocket { + fn addr_local(&self) -> Result { + VirtualSocket::addr_local(self) + } + + fn listen(&mut self) -> Result> { + match block_on(self.io_socket(RequestType::ListenBound)) { + ResponseType::Err(err) => Err(err), + ResponseType::None => { + let mut socket = self.transition_socket(); + socket.touch_begin_accept().ok(); + Ok(Box::new(socket)) + } + res => { + tracing::debug!("invalid response to listen bound request - {res:?}"); + Err(NetworkError::IOError) + } + } + } + + fn connect(&mut self, peer: SocketAddr) -> Result> { + match block_on(self.io_socket(RequestType::ConnectBound { peer })) { + ResponseType::Err(err) => Err(err), + ResponseType::None => Ok(Box::new(self.transition_socket())), + res => { + tracing::debug!("invalid response to connect bound request - {res:?}"); + Err(NetworkError::IOError) + } + } + } + + fn set_ttl(&mut self, ttl: u32) -> Result<()> { + VirtualSocket::set_ttl(self, ttl) + } + + fn ttl(&self) -> Result { + VirtualSocket::ttl(self) + } +} + impl VirtualRawSocket for RemoteSocket { fn try_send(&mut self, data: &[u8]) -> Result { let mut cx = Context::from_waker(&self.tx_waker); diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index 3f1af0ff4817..34443ea42211 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -4,7 +4,7 @@ use crate::ruleset::{Direction, Ruleset}; use crate::{ IpCidr, IpRoute, NetworkError, Result, SocketStatus, StreamSecurity, VirtualConnectedSocket, VirtualConnectionlessSocket, VirtualIcmpSocket, VirtualNetworking, VirtualRawSocket, - VirtualSocket, VirtualTcpListener, VirtualTcpSocket, VirtualUdpSocket, + VirtualSocket, VirtualTcpBoundSocket, VirtualTcpListener, VirtualTcpSocket, VirtualUdpSocket, }; use crate::{VirtualIoSource, io_err_into_net_error}; use bytes::{Buf, BytesMut}; @@ -66,6 +66,34 @@ impl Default for LocalNetworking { } } +fn sock_addr_into_socket_addr(addr: socket2::SockAddr) -> Result { + addr.as_socket().ok_or(NetworkError::UnknownError) +} + +fn tcp_socket_domain(addr: SocketAddr) -> socket2::Domain { + if addr.is_ipv4() { + socket2::Domain::IPV4 + } else { + socket2::Domain::IPV6 + } +} + +fn tcp_connect_in_progress(err: &io::Error) -> bool { + if matches!(err.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted) { + return true; + } + + #[cfg(all(target_family = "unix", feature = "libc"))] + { + return matches!(err.raw_os_error(), Some(libc::EINPROGRESS | libc::EALREADY)); + } + + #[cfg(not(all(target_family = "unix", feature = "libc")))] + { + false + } +} + #[async_trait::async_trait] #[allow(unused_variables)] impl VirtualNetworking for LocalNetworking { @@ -83,21 +111,45 @@ impl VirtualNetworking for LocalNetworking { return Err(NetworkError::PermissionDenied); } - let listener = std::net::TcpListener::bind(addr) - .map(|sock| { - sock.set_nonblocking(true).ok(); - Box::new(LocalTcpListener { - stream: mio::net::TcpListener::from_std(sock), - selector: self.selector.clone(), - handler_guard: HandlerGuardState::None, - no_delay: None, - keep_alive: None, - backlog: Default::default(), - ruleset: self.ruleset.clone(), - }) - }) + self.bind_tcp(addr, only_v6, reuse_port, reuse_addr) + .await? + .listen() + } + + async fn bind_tcp( + &self, + addr: SocketAddr, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, + ) -> Result> { + if let Some(ruleset) = self.ruleset.as_ref() + && !ruleset.allows_socket(addr, Direction::Inbound) + { + tracing::warn!(%addr, "bind_tcp blocked by firewall rule"); + return Err(NetworkError::PermissionDenied); + } + + let socket = socket2::Socket::new(tcp_socket_domain(addr), socket2::Type::STREAM, None) .map_err(io_err_into_net_error)?; - Ok(listener) + socket.set_nonblocking(true).map_err(io_err_into_net_error)?; + if addr.is_ipv6() { + socket.set_only_v6(only_v6).map_err(io_err_into_net_error)?; + } + socket + .set_reuse_address(reuse_addr) + .map_err(io_err_into_net_error)?; + #[cfg(not(windows))] + socket + .set_reuse_port(reuse_port) + .map_err(io_err_into_net_error)?; + socket.bind(&addr.into()).map_err(io_err_into_net_error)?; + + Ok(Box::new(LocalTcpBoundSocket { + socket: Some(socket), + selector: self.selector.clone(), + ruleset: self.ruleset.clone(), + })) } async fn bind_udp( @@ -377,6 +429,71 @@ impl VirtualIoSource for LocalTcpListener { } } +#[derive(Debug)] +pub struct LocalTcpBoundSocket { + socket: Option, + selector: Arc, + ruleset: Option, +} + +impl VirtualTcpBoundSocket for LocalTcpBoundSocket { + fn addr_local(&self) -> Result { + let socket = self.socket.as_ref().ok_or(NetworkError::InvalidFd)?; + let addr = socket.local_addr().map_err(io_err_into_net_error)?; + sock_addr_into_socket_addr(addr) + } + + fn listen(&mut self) -> Result> { + let socket = self.socket.take().ok_or(NetworkError::InvalidFd)?; + socket.listen(128).map_err(io_err_into_net_error)?; + let listener = mio::net::TcpListener::from_std(socket.into()); + Ok(Box::new(LocalTcpListener { + stream: listener, + selector: self.selector.clone(), + handler_guard: HandlerGuardState::None, + no_delay: None, + keep_alive: None, + backlog: Default::default(), + ruleset: self.ruleset.clone(), + })) + } + + fn connect(&mut self, mut peer: SocketAddr) -> Result> { + if let Some(ruleset) = self.ruleset.as_ref() + && !ruleset.allows_socket(peer, Direction::Outbound) + { + tracing::warn!(%peer, "bound connect_tcp blocked by firewall rule"); + return Err(NetworkError::PermissionDenied); + } + + let socket = self.socket.take().ok_or(NetworkError::InvalidFd)?; + if let Err(err) = socket.connect(&peer.into()) { + if !tcp_connect_in_progress(&err) { + return Err(io_err_into_net_error(err)); + } + } + + let stream = mio::net::TcpStream::from_std(socket.into()); + if let Ok(p) = stream.peer_addr() { + peer = p; + } + Ok(Box::new(LocalTcpStream::new( + self.selector.clone(), + stream, + peer, + ))) + } + + fn set_ttl(&mut self, ttl: u32) -> Result<()> { + let _ = ttl; + Err(NetworkError::Unsupported) + } + + fn ttl(&self) -> Result { + Err(NetworkError::Unsupported) + } +} + #[derive(Debug)] enum ConnectState { Unknown, diff --git a/lib/virtual-net/src/lib.rs b/lib/virtual-net/src/lib.rs index 22ae000a4996..6a2db3236491 100644 --- a/lib/virtual-net/src/lib.rs +++ b/lib/virtual-net/src/lib.rs @@ -183,6 +183,18 @@ pub trait VirtualNetworking: fmt::Debug + Send + Sync + 'static { Err(NetworkError::Unsupported) } + /// Binds a TCP socket to a specific IP and port without immediately + /// listening for connections or connecting to a peer. + async fn bind_tcp( + &self, + addr: SocketAddr, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, + ) -> Result> { + Err(NetworkError::Unsupported) + } + /// Opens a UDP socket that listens on a specific IP and Port combination /// Multiple servers (processes or threads) can bind to the same port if they each set /// the reuse-port and-or reuse-addr flags @@ -241,6 +253,23 @@ pub trait VirtualTcpListener: VirtualIoSource + fmt::Debug + Send + Sync + 'stat fn ttl(&self) -> Result; } +pub trait VirtualTcpBoundSocket: fmt::Debug + Send + Sync + 'static { + /// Returns the local address of this bound TCP socket. + fn addr_local(&self) -> Result; + + /// Places the socket into listening mode. + fn listen(&mut self) -> Result>; + + /// Initiates a TCP connection using the already-bound local address. + fn connect(&mut self, peer: SocketAddr) -> Result>; + + /// Sets how many network hops the packets are permitted for this socket. + fn set_ttl(&mut self, ttl: u32) -> Result<()>; + + /// Returns the maximum number of network hops before packets are dropped. + fn ttl(&self) -> Result; +} + #[async_trait::async_trait] pub trait VirtualTcpListenerExt: VirtualTcpListener { /// Accepts a new connection from the TCP listener diff --git a/lib/virtual-net/src/loopback.rs b/lib/virtual-net/src/loopback.rs index b59519790bcb..dfc15cdd7109 100644 --- a/lib/virtual-net/src/loopback.rs +++ b/lib/virtual-net/src/loopback.rs @@ -7,16 +7,28 @@ use std::{collections::HashMap, sync::Arc}; use crate::tcp_pair::TcpSocketHalf; use crate::{ InterestHandler, IpAddr, IpCidr, Ipv4Addr, Ipv6Addr, NetworkError, VirtualIoSource, - VirtualNetworking, VirtualTcpListener, VirtualTcpSocket, + VirtualNetworking, VirtualTcpBoundSocket, VirtualTcpListener, VirtualTcpSocket, }; use virtual_mio::InterestType; const DEFAULT_MAX_BUFFER_SIZE: usize = 1_048_576; +const LOOPBACK_EPHEMERAL_PORT_START: u16 = 49152; -#[derive(Debug, Default)] +#[derive(Debug)] struct LoopbackNetworkingState { tcp_listeners: HashMap, ip_addresses: Vec, + next_ephemeral_port: u16, +} + +impl Default for LoopbackNetworkingState { + fn default() -> Self { + Self { + tcp_listeners: HashMap::new(), + ip_addresses: Vec::new(), + next_ephemeral_port: LOOPBACK_EPHEMERAL_PORT_START, + } + } } #[derive(Debug, Clone)] @@ -62,6 +74,44 @@ impl LoopbackNetworking { .map(|listener| listener.1.connect_to(local_addr)) } } + + fn allocate_tcp_bind_addr(state: &mut LoopbackNetworkingState, mut addr: SocketAddr) -> SocketAddr { + if addr.port() == 0 { + let start = state.next_ephemeral_port; + let mut candidate = start; + loop { + let candidate_addr = SocketAddr::new(addr.ip(), candidate); + if !state.tcp_listeners.contains_key(&candidate_addr) { + addr.set_port(candidate); + state.next_ephemeral_port = if candidate == u16::MAX { + LOOPBACK_EPHEMERAL_PORT_START + } else { + candidate + 1 + }; + break; + } + + candidate = if candidate == u16::MAX { + LOOPBACK_EPHEMERAL_PORT_START + } else { + candidate + 1 + }; + if candidate == start { + break; + } + } + } + addr + } + + fn normalize_listener_addr(mut addr: SocketAddr) -> SocketAddr { + if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) { + addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), addr.port()); + } else if addr.ip() == IpAddr::V6(Ipv6Addr::UNSPECIFIED) { + addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), addr.port()); + } + addr + } } impl Default for LoopbackNetworking { @@ -115,23 +165,27 @@ impl VirtualNetworking for LoopbackNetworking { async fn listen_tcp( &self, - mut addr: SocketAddr, + addr: SocketAddr, _only_v6: bool, _reuse_port: bool, _reuse_addr: bool, ) -> crate::Result> { - let listener = LoopbackTcpListener::new(addr); - - if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) { - addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), addr.port()); - } else if addr.ip() == IpAddr::V6(Ipv6Addr::UNSPECIFIED) { - addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), addr.port()); - } + self.bind_tcp(addr, false, false, false).await?.listen() + } + async fn bind_tcp( + &self, + addr: SocketAddr, + _only_v6: bool, + _reuse_port: bool, + _reuse_addr: bool, + ) -> crate::Result> { let mut state = self.state.lock().unwrap(); - state.tcp_listeners.insert(addr, listener.clone()); - - Ok(Box::new(listener)) + let addr = Self::allocate_tcp_bind_addr(&mut state, addr); + Ok(Box::new(LoopbackTcpBoundSocket { + networking: self.clone(), + local_addr: addr, + })) } } @@ -235,3 +289,41 @@ impl VirtualTcpListener for LoopbackTcpListener { Ok(64) } } + +#[derive(Debug, Clone)] +pub struct LoopbackTcpBoundSocket { + networking: LoopbackNetworking, + local_addr: SocketAddr, +} + +impl VirtualTcpBoundSocket for LoopbackTcpBoundSocket { + fn addr_local(&self) -> crate::Result { + Ok(self.local_addr) + } + + fn listen(&mut self) -> crate::Result> { + let listener = LoopbackTcpListener::new(self.local_addr); + let mut state = self.networking.state.lock().unwrap(); + state.tcp_listeners.insert( + LoopbackNetworking::normalize_listener_addr(self.local_addr), + listener.clone(), + ); + Ok(Box::new(listener)) + } + + fn connect(&mut self, peer: SocketAddr) -> crate::Result> { + let socket = self + .networking + .loopback_connect_to(self.local_addr, peer) + .ok_or(NetworkError::ConnectionRefused)?; + Ok(Box::new(socket)) + } + + fn set_ttl(&mut self, _ttl: u32) -> crate::Result<()> { + Err(NetworkError::Unsupported) + } + + fn ttl(&self) -> crate::Result { + Err(NetworkError::Unsupported) + } +} diff --git a/lib/virtual-net/src/meta.rs b/lib/virtual-net/src/meta.rs index 7a8f516a8c54..1c62af53353b 100644 --- a/lib/virtual-net/src/meta.rs +++ b/lib/virtual-net/src/meta.rs @@ -97,6 +97,14 @@ pub enum RequestType { reuse_port: bool, reuse_addr: bool, }, + /// Binds a TCP socket without immediately listening or connecting. + BindTcp { + socket_id: SocketId, + addr: SocketAddr, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, + }, /// Opens a UDP socket that listens on a specific IP and Port combination /// Multiple servers (processes or threads) can bind to the same port if they each set /// the reuse-port and-or reuse-addr flags @@ -123,6 +131,10 @@ pub enum RequestType { }, /// Closes the socket Close, + /// Converts a bound TCP socket into a listening socket. + ListenBound, + /// Converts a bound TCP socket into a connected TCP stream. + ConnectBound { peer: SocketAddr }, /// Begins the process of accepting a socket and returns it later BeginAccept(SocketId), /// Returns the local address of this TCP listener diff --git a/lib/virtual-net/src/server.rs b/lib/virtual-net/src/server.rs index 7a0b8a6c6feb..bae93c2af583 100644 --- a/lib/virtual-net/src/server.rs +++ b/lib/virtual-net/src/server.rs @@ -1,8 +1,9 @@ use crate::meta::{FrameSerializationFormat, ResponseType}; use crate::rx_tx::{RemoteRx, RemoteTx, RemoteTxWakers}; -use crate::{IpCidr, IpRoute, NetworkError, StreamSecurity, VirtualIcmpSocket}; +use crate::{IpCidr, IpRoute, NetworkError, SocketStatus, StreamSecurity, VirtualIcmpSocket}; use crate::{ - VirtualNetworking, VirtualRawSocket, VirtualTcpListener, VirtualTcpSocket, VirtualUdpSocket, + VirtualNetworking, VirtualRawSocket, VirtualTcpBoundSocket, VirtualTcpListener, + VirtualTcpSocket, VirtualUdpSocket, meta::{MessageRequest, MessageResponse, RequestType, SocketId}, }; use futures_util::stream::FuturesOrdered; @@ -534,6 +535,7 @@ impl RemoteNetworkingServerDriver { // a child ID we can actually use Ok(()) } + RemoteAdapterSocket::BoundTcp(_) => Ok(()), RemoteAdapterSocket::TcpSocket(s) => s.set_handler(handler), RemoteAdapterSocket::UdpSocket(s) => s.set_handler(handler), RemoteAdapterSocket::IcmpSocket(s) => s.set_handler(handler), @@ -756,6 +758,23 @@ impl RemoteNetworkingServerDriver { socket_id, req_id, ), + RequestType::BindTcp { + socket_id, + addr, + only_v6, + reuse_port, + reuse_addr, + } => self.process_async_new_socket( + move |inner: Arc| async move { + Ok(RemoteAdapterSocket::BoundTcp( + inner + .bind_tcp(addr, only_v6, reuse_port, reuse_addr) + .await?, + )) + }, + socket_id, + req_id, + ), RequestType::ListenTcp { socket_id, addr, @@ -857,11 +876,69 @@ impl RemoteNetworkingServerDriver { socket_id, req_id, ), + RequestType::ListenBound => { + let res = { + let mut guard = self.common.sockets.lock().unwrap(); + match guard.get_mut(&socket_id) { + Some(socket) => match socket { + RemoteAdapterSocket::BoundTcp(bound) => match bound.listen() { + Ok(listener) => { + *socket = RemoteAdapterSocket::TcpListener { + socket: listener, + next_accept: None, + }; + Ok(()) + } + Err(err) => Err(err), + }, + _ => Err(NetworkError::Unsupported), + }, + _ => Err(NetworkError::Unsupported), + } + }; + req_id.and_then(|req_id| { + self.common.send(MessageResponse::ResponseToRequest { + req_id, + res: match res { + Ok(()) => ResponseType::None, + Err(err) => ResponseType::Err(err), + }, + }) + }) + } + RequestType::ConnectBound { peer } => { + let res = { + let mut guard = self.common.sockets.lock().unwrap(); + match guard.get_mut(&socket_id) { + Some(socket) => match socket { + RemoteAdapterSocket::BoundTcp(bound) => match bound.connect(peer) { + Ok(connected) => { + *socket = RemoteAdapterSocket::TcpSocket(connected); + Ok(()) + } + Err(err) => Err(err), + }, + _ => Err(NetworkError::Unsupported), + }, + _ => Err(NetworkError::Unsupported), + } + }; + req_id.and_then(|req_id| { + self.common.send(MessageResponse::ResponseToRequest { + req_id, + res: match res { + Ok(()) => ResponseType::None, + Err(err) => ResponseType::Err(err), + }, + }) + }) + } RequestType::BeginAccept(child_id) => { self.process_inner_begin_accept(socket_id, child_id, req_id) } RequestType::GetAddrLocal => self.process_inner( move |socket| match socket { + RemoteAdapterSocket::BoundTcp(s) => s.addr_local(), RemoteAdapterSocket::TcpSocket(s) => s.addr_local(), RemoteAdapterSocket::TcpListener { socket: s, .. } => s.addr_local(), RemoteAdapterSocket::UdpSocket(s) => s.addr_local(), @@ -877,6 +954,7 @@ impl RemoteNetworkingServerDriver { ), RequestType::GetAddrPeer => self.process_inner( move |socket| match socket { + RemoteAdapterSocket::BoundTcp(_) => Err(NetworkError::Unsupported), RemoteAdapterSocket::TcpSocket(s) => s.addr_peer().map(Some), RemoteAdapterSocket::TcpListener { .. } => Err(NetworkError::Unsupported), RemoteAdapterSocket::UdpSocket(s) => s.addr_peer(), @@ -893,6 +971,7 @@ impl RemoteNetworkingServerDriver { ), RequestType::SetTtl(ttl) => self.process_inner_noop( move |socket| match socket { + RemoteAdapterSocket::BoundTcp(s) => s.set_ttl(ttl), RemoteAdapterSocket::TcpSocket(s) => s.set_ttl(ttl), RemoteAdapterSocket::TcpListener { socket: s, .. } => { s.set_ttl(ttl.try_into().unwrap_or_default()) @@ -906,6 +985,7 @@ impl RemoteNetworkingServerDriver { ), RequestType::GetTtl => self.process_inner( move |socket| match socket { + RemoteAdapterSocket::BoundTcp(s) => s.ttl(), RemoteAdapterSocket::TcpSocket(s) => s.ttl(), RemoteAdapterSocket::TcpListener { socket: s, .. } => s.ttl().map(|t| t as u32), RemoteAdapterSocket::UdpSocket(s) => s.ttl(), @@ -921,6 +1001,7 @@ impl RemoteNetworkingServerDriver { ), RequestType::GetStatus => self.process_inner( move |socket| match socket { + RemoteAdapterSocket::BoundTcp(_) => Ok(SocketStatus::Opened), RemoteAdapterSocket::TcpSocket(s) => s.status(), RemoteAdapterSocket::TcpListener { .. } => Err(NetworkError::Unsupported), RemoteAdapterSocket::UdpSocket(s) => s.status(), @@ -1227,6 +1308,7 @@ impl RemoteNetworkingServerDriver { #[derive(Debug)] enum RemoteAdapterSocket { + BoundTcp(Box), TcpListener { socket: Box, next_accept: Option, @@ -1414,6 +1496,7 @@ impl RemoteAdapterSocket { let mut ret: FuturesOrdered> = Default::default(); loop { break match self { + Self::BoundTcp(_) => {} Self::TcpListener { socket, next_accept, diff --git a/lib/virtual-net/src/tests.rs b/lib/virtual-net/src/tests.rs index 9ed1a46d8e14..78cb138966ef 100644 --- a/lib/virtual-net/src/tests.rs +++ b/lib/virtual-net/src/tests.rs @@ -122,6 +122,33 @@ async fn test_tcp(client: RemoteNetworkingClient, _server: RemoteNetworkingServe tracing::info!("all good"); } +#[cfg(feature = "remote")] +async fn test_bound_tcp(client: RemoteNetworkingClient, _server: RemoteNetworkingServer) { + let mut bound = client + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + let addr_after_bind = bound.addr_local().unwrap(); + assert_ne!( + addr_after_bind.port(), + 0, + "remote bind_tcp should allocate a real ephemeral port before listen" + ); + + let listener = bound.listen().unwrap(); + let addr_after_listen = listener.addr_local().unwrap(); + assert_eq!( + addr_after_listen, addr_after_bind, + "remote listen should preserve the already-bound local address" + ); +} + #[cfg(feature = "remote")] #[cfg_attr(windows, ignore)] #[traced_test] @@ -132,6 +159,16 @@ async fn test_tcp_with_mpsc() { test_tcp(client, server).await } +#[cfg(feature = "remote")] +#[cfg_attr(windows, ignore)] +#[traced_test] +#[tokio::test(flavor = "multi_thread")] +#[serial_test::serial] +async fn test_bound_tcp_with_mpsc() { + let (client, server) = setup_mpsc().await; + test_bound_tcp(client, server).await +} + // Disabled on musl due to flakiness. // See https://github.com/wasmerio/wasmer/issues/4425 #[cfg(not(target_env = "musl"))] @@ -548,3 +585,79 @@ async fn test_failed_connect_status_stays_failed() { assert!(matches!(socket.status().unwrap(), SocketStatus::Failed)); } + +#[cfg(not(target_os = "windows"))] +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_bind_tcp_assigns_ephemeral_port_before_listen() { + let networking = LocalNetworking::new(); + let mut bound = networking + .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .await + .unwrap(); + + let addr_after_bind = bound.addr_local().unwrap(); + assert_ne!( + addr_after_bind.port(), + 0, + "bind_tcp should allocate a real ephemeral port before listen" + ); + + let listener = bound.listen().unwrap(); + let addr_after_listen = listener.addr_local().unwrap(); + assert_eq!( + addr_after_listen, addr_after_bind, + "listen should preserve the already-bound local address" + ); +} + +#[cfg(not(target_os = "windows"))] +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_bind_tcp_keeps_same_port_across_connect() { + let probe = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let peer = probe.local_addr().unwrap(); + + let networking = LocalNetworking::new(); + let mut bound = networking + .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .await + .unwrap(); + + let addr_after_bind = bound.addr_local().unwrap(); + assert_ne!(addr_after_bind.port(), 0); + + let socket = bound.connect(peer).unwrap(); + let addr_after_connect = socket.addr_local().unwrap(); + assert_eq!( + addr_after_connect, addr_after_bind, + "connect should preserve the already-bound local address" + ); +} + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_assigns_ephemeral_port_before_listen() { + let networking = LoopbackNetworking::new(); + let mut bound = networking + .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .await + .unwrap(); + + let addr_after_bind = bound.addr_local().unwrap(); + assert_ne!( + addr_after_bind.port(), + 0, + "loopback bind_tcp should allocate a real ephemeral port before listen" + ); + + let listener = bound.listen().unwrap(); + let addr_after_listen = listener.addr_local().unwrap(); + assert_eq!( + addr_after_listen, addr_after_bind, + "loopback listen should preserve the already-bound local address" + ); +} diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index 767a0e29dd62..8ddb8c9f0e8e 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -13,8 +13,8 @@ use std::{ use serde_derive::{Deserialize, Serialize}; use virtual_mio::InterestHandler; use virtual_net::{ - NetworkError, VirtualIcmpSocket, VirtualNetworking, VirtualRawSocket, VirtualTcpListener, - VirtualTcpSocket, VirtualUdpSocket, net_error_into_io_err, + NetworkError, VirtualIcmpSocket, VirtualNetworking, VirtualRawSocket, VirtualTcpBoundSocket, + VirtualTcpListener, VirtualTcpSocket, VirtualUdpSocket, net_error_into_io_err, }; use wasmer_types::MemorySize; use wasmer_wasix_types::wasi::{Addressfamily, Errno, Rights, SockProto, Sockoption, Socktype}; @@ -52,6 +52,29 @@ pub struct SocketProperties { pub handler: Option>, } +impl SocketProperties { + fn placeholder_from(existing: &Self) -> Self { + Self { + family: existing.family, + ty: existing.ty, + pt: existing.pt, + only_v6: false, + reuse_port: false, + reuse_addr: false, + no_delay: None, + keep_alive: None, + dont_route: None, + send_buf_size: None, + recv_buf_size: None, + write_timeout: None, + read_timeout: None, + accept_timeout: None, + connect_timeout: None, + handler: None, + } + } +} + #[derive(Debug)] //#[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))] pub enum InodeSocketKind { @@ -70,6 +93,10 @@ pub enum InodeSocketKind { write_timeout: Option, read_timeout: Option, }, + BoundTcp { + socket: Box, + props: SocketProperties, + }, UdpSocket { socket: Box, peer: Option, @@ -319,9 +346,25 @@ impl InodeSocket { match props.ty { Socktype::Stream => { - // we already set the socket address - next we need a listen or connect so nothing - // more to do at this time - return Ok(None); + let only_v6 = props.only_v6; + let reuse_port = props.reuse_port; + let reuse_addr = props.reuse_addr; + match net.bind_tcp(addr, only_v6, reuse_port, reuse_addr).await { + Ok(socket) => { + let placeholder = SocketProperties::placeholder_from(props); + let props = std::mem::replace(props, placeholder); + return Ok(Some(InodeSocket::new(InodeSocketKind::BoundTcp { + socket, + props, + }))); + } + Err(NetworkError::Unsupported) => { + // Fallback for backends that still only materialize TCP state at + // listen/connect time. + return Ok(None); + } + Err(err) => return Err(net_error_into_wasi_err(err)), + } } Socktype::Dgram => { let reuse_port = props.reuse_port; @@ -377,6 +420,7 @@ impl InodeSocket { _ => return Err(Errno::Inval), } } + InodeSocketKind::BoundTcp { .. } => return Err(Errno::Inval), _ => return Err(Errno::Notsup), } }; @@ -405,8 +449,8 @@ impl InodeSocket { .unwrap_or(Duration::from_secs(30)); let socket = { - let inner = self.inner.protected.read().unwrap(); - match &inner.kind { + let mut inner = self.inner.protected.write().unwrap(); + match &mut inner.kind { InodeSocketKind::PreSocket { props, addr, .. } => match props.ty { Socktype::Stream => { if addr.is_none() { @@ -451,6 +495,12 @@ impl InodeSocket { return Err(Errno::Notsup); } }, + InodeSocketKind::BoundTcp { socket, .. } => { + return Ok(Some(InodeSocket::new(InodeSocketKind::TcpListener { + socket: socket.listen().map_err(net_error_into_wasi_err)?, + accept_timeout: Some(timeout), + }))); + } InodeSocketKind::Icmp(_) => { tracing::warn!("wasi[?]::sock_listen - failed - not supported(icmp)"); return Err(Errno::Notsup); @@ -562,6 +612,7 @@ impl InodeSocket { InodeSocketKind::TcpStream { socket, .. } => { socket.close().map_err(net_error_into_wasi_err)?; } + InodeSocketKind::BoundTcp { .. } => {} InodeSocketKind::Icmp(_) => {} InodeSocketKind::UdpSocket { .. } => {} InodeSocketKind::Raw(_) => {} @@ -585,7 +636,9 @@ impl InodeSocket { let timeout = timeout.unwrap_or(Duration::from_secs(30)); let handler; - let connect = { + let connect: Pin< + Box, Errno>> + '_>, + > = { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, addr, .. } => { @@ -608,7 +661,10 @@ impl InodeSocket { } }; Box::pin(async move { - let mut ret = net.connect_tcp(addr, peer).await?; + let mut ret = net + .connect_tcp(addr, peer) + .await + .map_err(net_error_into_wasi_err)?; if let Some(no_delay) = no_delay { ret.set_nodelay(no_delay).ok(); } @@ -619,7 +675,42 @@ impl InodeSocket { ret.set_dontroute(dont_route).ok(); } if !nonblocking { - futures::future::poll_fn(|cx| ret.poll_write_ready(cx)).await?; + futures::future::poll_fn(|cx| ret.poll_write_ready(cx)) + .await + .map_err(net_error_into_wasi_err)?; + } + Ok(ret) + }) + } + Socktype::Dgram => return Err(Errno::Inval), + _ => return Err(Errno::Notsup), + } + } + InodeSocketKind::BoundTcp { socket, props } => { + handler = props.handler.take(); + new_write_timeout = props.write_timeout; + new_read_timeout = props.read_timeout; + match props.ty { + Socktype::Stream => { + let no_delay = props.no_delay; + let keep_alive = props.keep_alive; + let dont_route = props.dont_route; + let mut ret = + socket.connect(peer).map_err(net_error_into_wasi_err)?; + if let Some(no_delay) = no_delay { + ret.set_nodelay(no_delay).ok(); + } + if let Some(keep_alive) = keep_alive { + ret.set_keepalive(keep_alive).ok(); + } + if let Some(dont_route) = dont_route { + ret.set_dontroute(dont_route).ok(); + } + Box::pin(async move { + if !nonblocking { + futures::future::poll_fn(|cx| ret.poll_write_ready(cx)) + .await + .map_err(net_error_into_wasi_err)?; } Ok(ret) }) @@ -643,7 +734,7 @@ impl InodeSocket { }; let mut socket = tokio::select! { - res = connect => res.map_err(net_error_into_wasi_err)?, + res = connect => res?, _ = tasks.sleep_now(timeout) => return Err(Errno::Timedout) }; @@ -666,6 +757,7 @@ impl InodeSocket { let inner = self.inner.protected.read().unwrap(); Ok(match &inner.kind { InodeSocketKind::PreSocket { .. } => WasiSocketStatus::Opening, + InodeSocketKind::BoundTcp { .. } => WasiSocketStatus::Opened, InodeSocketKind::TcpListener { .. } => WasiSocketStatus::Opened, InodeSocketKind::TcpStream { socket, .. } => match socket.status() { Ok(virtual_net::SocketStatus::Opening) => WasiSocketStatus::Opening, @@ -707,6 +799,9 @@ impl InodeSocket { InodeSocketKind::TcpStream { socket, .. } => { socket.addr_local().map_err(net_error_into_wasi_err)? } + InodeSocketKind::BoundTcp { socket, .. } => { + socket.addr_local().map_err(net_error_into_wasi_err)? + } InodeSocketKind::UdpSocket { socket, .. } => { socket.addr_local().map_err(net_error_into_wasi_err)? } @@ -728,6 +823,14 @@ impl InodeSocket { }, 0, ), + InodeSocketKind::BoundTcp { props, .. } => SocketAddr::new( + match props.family { + Addressfamily::Inet4 => IpAddr::V4(Ipv4Addr::UNSPECIFIED), + Addressfamily::Inet6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED), + _ => return Err(Errno::Inval), + }, + 0, + ), InodeSocketKind::TcpStream { socket, .. } => { socket.addr_peer().map_err(net_error_into_wasi_err)? } @@ -758,6 +861,7 @@ impl InodeSocket { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { match option { WasiSocketOption::OnlyV6 => props.only_v6 = val, @@ -809,6 +913,7 @@ impl InodeSocket { let mut inner = self.inner.protected.write().unwrap(); Ok(match &mut inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => match option { WasiSocketOption::OnlyV6 => props.only_v6, WasiSocketOption::ReusePort => props.reuse_port, @@ -853,6 +958,7 @@ impl InodeSocket { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { props.send_buf_size = Some(size); } @@ -870,6 +976,7 @@ impl InodeSocket { let inner = self.inner.protected.read().unwrap(); match &inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { Ok(props.send_buf_size.unwrap_or_default()) } @@ -884,6 +991,7 @@ impl InodeSocket { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { props.recv_buf_size = Some(size); } @@ -901,6 +1009,7 @@ impl InodeSocket { let inner = self.inner.protected.read().unwrap(); match &inner.kind { InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { Ok(props.recv_buf_size.unwrap_or_default()) } @@ -918,6 +1027,7 @@ impl InodeSocket { socket.set_linger(linger).map_err(net_error_into_wasi_err) } InodeSocketKind::RemoteSocket { .. } => Ok(()), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -929,6 +1039,7 @@ impl InodeSocket { InodeSocketKind::TcpStream { socket, .. } => { socket.linger().map_err(net_error_into_wasi_err) } + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -961,6 +1072,7 @@ impl InodeSocket { Ok(()) } InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { match ty { TimeType::ConnectTimeout => props.connect_timeout = timeout, @@ -992,6 +1104,7 @@ impl InodeSocket { _ => return Err(Errno::Inval), }), InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => match ty { TimeType::ConnectTimeout => Ok(props.connect_timeout), TimeType::AcceptTimeout => Ok(props.accept_timeout), @@ -1006,6 +1119,9 @@ impl InodeSocket { pub fn set_ttl(&self, ttl: u32) -> Result<(), Errno> { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { + InodeSocketKind::BoundTcp { socket, .. } => { + socket.set_ttl(ttl).map_err(net_error_into_wasi_err) + } InodeSocketKind::TcpStream { socket, .. } => { socket.set_ttl(ttl).map_err(net_error_into_wasi_err) } @@ -1024,6 +1140,9 @@ impl InodeSocket { pub fn ttl(&self) -> Result { let inner = self.inner.protected.read().unwrap(); match &inner.kind { + InodeSocketKind::BoundTcp { socket, .. } => { + socket.ttl().map_err(net_error_into_wasi_err) + } InodeSocketKind::TcpStream { socket, .. } => { socket.ttl().map_err(net_error_into_wasi_err) } @@ -1049,6 +1168,7 @@ impl InodeSocket { *set_ttl = ttl; Ok(()) } + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1061,6 +1181,7 @@ impl InodeSocket { socket.multicast_ttl_v4().map_err(net_error_into_wasi_err) } InodeSocketKind::RemoteSocket { multicast_ttl, .. } => Ok(*multicast_ttl), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1073,6 +1194,7 @@ impl InodeSocket { .join_multicast_v4(multiaddr, iface) .map_err(net_error_into_wasi_err), InodeSocketKind::RemoteSocket { .. } => Ok(()), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1085,6 +1207,7 @@ impl InodeSocket { .leave_multicast_v4(multiaddr, iface) .map_err(net_error_into_wasi_err), InodeSocketKind::RemoteSocket { .. } => Ok(()), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1097,6 +1220,7 @@ impl InodeSocket { .join_multicast_v6(multiaddr, iface) .map_err(net_error_into_wasi_err), InodeSocketKind::RemoteSocket { .. } => Ok(()), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1109,6 +1233,7 @@ impl InodeSocket { .leave_multicast_v6(multiaddr, iface) .map_err(net_error_into_wasi_err), InodeSocketKind::RemoteSocket { .. } => Ok(()), + InodeSocketKind::BoundTcp { .. } => Err(Errno::Io), InodeSocketKind::PreSocket { .. } => Err(Errno::Io), _ => Err(Errno::Notsup), } @@ -1477,6 +1602,7 @@ impl InodeSocket { socket.shutdown(how).map_err(net_error_into_wasi_err)?; } InodeSocketKind::RemoteSocket { .. } => return Ok(()), + InodeSocketKind::BoundTcp { .. } => return Err(Errno::Notconn), InodeSocketKind::PreSocket { .. } => return Err(Errno::Notconn), _ => return Err(Errno::Notsup), } @@ -1488,6 +1614,7 @@ impl InodeSocket { #[allow(clippy::match_like_matches_macro)] match &mut guard.kind { InodeSocketKind::TcpStream { .. } + | InodeSocketKind::BoundTcp { .. } | InodeSocketKind::UdpSocket { .. } | InodeSocketKind::Raw(..) => true, InodeSocketKind::RemoteSocket { is_dead, .. } => !(*is_dead), @@ -1510,6 +1637,9 @@ impl InodeSocketProtected { InodeSocketKind::PreSocket { props, .. } => { props.handler.take(); } + InodeSocketKind::BoundTcp { props, .. } => { + props.handler.take(); + } InodeSocketKind::RemoteSocket { props, .. } => { props.handler.take(); } @@ -1523,6 +1653,7 @@ impl InodeSocketProtected { InodeSocketKind::UdpSocket { socket, .. } => socket.poll_read_ready(cx), InodeSocketKind::Raw(socket) => socket.poll_read_ready(cx), InodeSocketKind::Icmp(socket) => socket.poll_read_ready(cx), + InodeSocketKind::BoundTcp { .. } => Poll::Pending, InodeSocketKind::PreSocket { .. } => Poll::Pending, InodeSocketKind::RemoteSocket { is_dead, .. } => match is_dead { true => Poll::Ready(Ok(0)), @@ -1539,6 +1670,7 @@ impl InodeSocketProtected { InodeSocketKind::UdpSocket { socket, .. } => socket.poll_write_ready(cx), InodeSocketKind::Raw(socket) => socket.poll_write_ready(cx), InodeSocketKind::Icmp(socket) => socket.poll_write_ready(cx), + InodeSocketKind::BoundTcp { .. } => Poll::Pending, InodeSocketKind::PreSocket { .. } => Poll::Pending, InodeSocketKind::RemoteSocket { is_dead, .. } => match is_dead { true => Poll::Ready(Ok(0)), @@ -1559,6 +1691,7 @@ impl InodeSocketProtected { InodeSocketKind::Raw(socket) => socket.set_handler(handler), InodeSocketKind::Icmp(socket) => socket.set_handler(handler), InodeSocketKind::PreSocket { props, .. } + | InodeSocketKind::BoundTcp { props, .. } | InodeSocketKind::RemoteSocket { props, .. } => { props.handler.replace(handler); Ok(()) diff --git a/lib/wasix/src/syscalls/wasix/sock_bind.rs b/lib/wasix/src/syscalls/wasix/sock_bind.rs index 37bec2ac69d5..8a5af867d049 100644 --- a/lib/wasix/src/syscalls/wasix/sock_bind.rs +++ b/lib/wasix/src/syscalls/wasix/sock_bind.rs @@ -28,7 +28,13 @@ pub fn sock_bind( #[cfg(feature = "journal")] if ctx.data().enable_journal { - JournalEffector::save_sock_bind(&mut ctx, sock, addr).map_err(|err| { + let effective_addr = wasi_try_ok!(__sock_actor( + &mut ctx, + sock, + Rights::empty(), + |socket, _| socket.addr_local() + )); + JournalEffector::save_sock_bind(&mut ctx, sock, effective_addr).map_err(|err| { tracing::error!("failed to save sock_bind event - {}", err); WasiError::Exit(ExitCode::from(Errno::Fault)) })?; diff --git a/lib/wasix/tests/wasm_tests/socket_tests.rs b/lib/wasix/tests/wasm_tests/socket_tests.rs index 7c624923631f..0173d1cfd4b4 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests.rs +++ b/lib/wasix/tests/wasm_tests/socket_tests.rs @@ -30,6 +30,38 @@ fn test_nonblocking_connect() { ); } +#[test] +// https://github.com/wasmerio/wasmer/issues/6403 +fn test_bind_port_zero_allocates_ephemeral_port() { + let wasm = run_build_script(file!(), "bind-port-zero").unwrap(); + let result = run_wasm_with_result(&wasm, wasm.parent().unwrap()).unwrap(); + let stdout = String::from_utf8_lossy(&result.stdout); + assert_eq!( + stdout.trim(), + "bind port 0 allocates an ephemeral port", + "exit_code={:?}\nstdout:\n{}\nstderr:\n{}", + result.exit_code, + stdout, + String::from_utf8_lossy(&result.stderr) + ); +} + +#[test] +// https://github.com/wasmerio/wasmer/issues/6403 +fn test_bind_port_zero_keeps_same_port_across_connect() { + let wasm = run_build_script(file!(), "bind-port-zero-connect").unwrap(); + let result = run_wasm_with_result(&wasm, wasm.parent().unwrap()).unwrap(); + let stdout = String::from_utf8_lossy(&result.stdout); + assert_eq!( + stdout.trim(), + "bind port 0 keeps the same ephemeral port across connect", + "exit_code={:?}\nstdout:\n{}\nstderr:\n{}", + result.exit_code, + stdout, + String::from_utf8_lossy(&result.stderr) + ); +} + #[test] // https://github.com/wasmerio/wasmer/issues/6366 #[ignore = "flaky test (#6366)"] diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/build.sh b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/build.sh new file mode 100644 index 000000000000..b3d2a483d5fd --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +$CC main.c -o main diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c new file mode 100644 index 000000000000..742ce91e2518 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c @@ -0,0 +1,119 @@ +#include +#include +#include +#include +#include + +static int get_local_addr(int fd, struct sockaddr_in* addr) { + socklen_t len = sizeof(*addr); + memset(addr, 0, sizeof(*addr)); + return getsockname(fd, (struct sockaddr*)addr, &len); +} + +int main(void) { + int server_fd = socket(AF_INET, SOCK_STREAM, 0); + if (server_fd < 0) { + perror("socket(server)"); + return 1; + } + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(0); + if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) != 1) { + fprintf(stderr, "inet_pton(server) failed\n"); + close(server_fd); + return 1; + } + + if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + perror("bind(server)"); + close(server_fd); + return 1; + } + + if (listen(server_fd, 1) < 0) { + perror("listen(server)"); + close(server_fd); + return 1; + } + + struct sockaddr_in server_bound_addr; + if (get_local_addr(server_fd, &server_bound_addr) < 0) { + perror("getsockname(server)"); + close(server_fd); + return 1; + } + + int client_fd = socket(AF_INET, SOCK_STREAM, 0); + if (client_fd < 0) { + perror("socket(client)"); + close(server_fd); + return 1; + } + + struct sockaddr_in client_bind_addr; + memset(&client_bind_addr, 0, sizeof(client_bind_addr)); + client_bind_addr.sin_family = AF_INET; + client_bind_addr.sin_port = htons(0); + if (inet_pton(AF_INET, "127.0.0.1", &client_bind_addr.sin_addr) != 1) { + fprintf(stderr, "inet_pton(client) failed\n"); + close(client_fd); + close(server_fd); + return 1; + } + + if (bind(client_fd, (struct sockaddr*)&client_bind_addr, sizeof(client_bind_addr)) < 0) { + perror("bind(client)"); + close(client_fd); + close(server_fd); + return 1; + } + + struct sockaddr_in client_after_bind; + if (get_local_addr(client_fd, &client_after_bind) < 0) { + perror("getsockname(client after bind)"); + close(client_fd); + close(server_fd); + return 1; + } + + int bind_port = ntohs(client_after_bind.sin_port); + if (bind_port == 0) { + fprintf(stderr, "expected nonzero client port after bind, got 0\n"); + close(client_fd); + close(server_fd); + return 1; + } + + if (connect(client_fd, (struct sockaddr*)&server_bound_addr, sizeof(server_bound_addr)) < 0) { + perror("connect(client)"); + close(client_fd); + close(server_fd); + return 1; + } + + struct sockaddr_in client_after_connect; + if (get_local_addr(client_fd, &client_after_connect) < 0) { + perror("getsockname(client after connect)"); + close(client_fd); + close(server_fd); + return 1; + } + + int connect_port = ntohs(client_after_connect.sin_port); + if (connect_port != bind_port) { + fprintf(stderr, + "expected client port to stay stable across connect, got %d then %d\n", + bind_port, connect_port); + close(client_fd); + close(server_fd); + return 1; + } + + puts("bind port 0 keeps the same ephemeral port across connect"); + close(client_fd); + close(server_fd); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/build.sh b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/build.sh new file mode 100644 index 000000000000..b3d2a483d5fd --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +$CC main.c -o main diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/main.c b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/main.c new file mode 100644 index 000000000000..3aa0dfe7a69c --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero/main.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include + +static int get_local_addr(int fd, struct sockaddr_in* addr) { + socklen_t len = sizeof(*addr); + memset(addr, 0, sizeof(*addr)); + return getsockname(fd, (struct sockaddr*)addr, &len); +} + +int main(void) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_in bind_addr; + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_port = htons(0); + if (inet_pton(AF_INET, "127.0.0.1", &bind_addr.sin_addr) != 1) { + fprintf(stderr, "inet_pton failed\n"); + close(fd); + return 1; + } + + if (bind(fd, (struct sockaddr*)&bind_addr, sizeof(bind_addr)) < 0) { + perror("bind"); + close(fd); + return 1; + } + + struct sockaddr_in after_bind; + if (get_local_addr(fd, &after_bind) < 0) { + perror("getsockname(after bind)"); + close(fd); + return 1; + } + + int bind_port = ntohs(after_bind.sin_port); + if (bind_port == 0) { + fprintf(stderr, "expected nonzero ephemeral port after bind, got 0\n"); + close(fd); + return 1; + } + + if (listen(fd, 1) < 0) { + perror("listen"); + close(fd); + return 1; + } + + struct sockaddr_in after_listen; + if (get_local_addr(fd, &after_listen) < 0) { + perror("getsockname(after listen)"); + close(fd); + return 1; + } + + int listen_port = ntohs(after_listen.sin_port); + if (listen_port != bind_port) { + fprintf(stderr, + "expected port to stay stable after listen, got %d then %d\n", + bind_port, listen_port); + close(fd); + return 1; + } + + puts("bind port 0 allocates an ephemeral port"); + close(fd); + return 0; +} From ca4c1c6fd7ae67a79e1118023912bbd3a038f05f Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Wed, 15 Apr 2026 14:02:35 +0000 Subject: [PATCH 03/13] fix: tighten tcp bind and socket lifecycle handling --- lib/virtual-net/src/client.rs | 16 +- lib/virtual-net/src/host.rs | 29 +- lib/virtual-net/src/loopback.rs | 119 ++++++-- lib/virtual-net/src/server.rs | 33 +- lib/virtual-net/src/tests.rs | 283 +++++++++++++++++- lib/wasix/src/net/socket.rs | 264 ++++++++++++---- .../bind-port-zero-connect/main.c | 16 +- 7 files changed, 655 insertions(+), 105 deletions(-) diff --git a/lib/virtual-net/src/client.rs b/lib/virtual-net/src/client.rs index e84854b3e2dc..96922ddaf872 100644 --- a/lib/virtual-net/src/client.rs +++ b/lib/virtual-net/src/client.rs @@ -922,6 +922,14 @@ impl Drop for RemoteSocket { if !self.owns_socket_bindings { return; } + let _ = self.io_socket_fire_and_forget(RequestType::Close); + self.release_socket_bindings(); + } +} + +impl RemoteSocket { + fn release_socket_bindings(&mut self) { + self.owns_socket_bindings = false; self.common.recv_tx.lock().unwrap().remove(&self.socket_id); self.common .recv_with_addr_tx @@ -929,9 +937,7 @@ impl Drop for RemoteSocket { .unwrap() .remove(&self.socket_id); } -} -impl RemoteSocket { async fn io_socket(&self, req: RequestType) -> ResponseType { let req_id = self.common.request_seed.fetch_add(1, Ordering::SeqCst); let mut req_rx = { @@ -1536,7 +1542,11 @@ impl VirtualConnectedSocket for RemoteSocket { } fn close(&mut self) -> Result<()> { - self.io_socket_fire_and_forget(RequestType::Close) + let ret = self.io_socket_fire_and_forget(RequestType::Close); + if ret.is_ok() { + self.release_socket_bindings(); + } + ret } fn try_recv(&mut self, buf: &mut [std::mem::MaybeUninit], peek: bool) -> Result { diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index 34443ea42211..ad02868f493d 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -79,13 +79,19 @@ fn tcp_socket_domain(addr: SocketAddr) -> socket2::Domain { } fn tcp_connect_in_progress(err: &io::Error) -> bool { - if matches!(err.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted) { + if matches!( + err.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted + ) { return true; } #[cfg(all(target_family = "unix", feature = "libc"))] { - return matches!(err.raw_os_error(), Some(libc::EINPROGRESS | libc::EALREADY)); + return matches!( + err.raw_os_error(), + Some(raw) if raw == libc::EINPROGRESS || raw == libc::EALREADY + ); } #[cfg(not(all(target_family = "unix", feature = "libc")))] @@ -132,7 +138,9 @@ impl VirtualNetworking for LocalNetworking { let socket = socket2::Socket::new(tcp_socket_domain(addr), socket2::Type::STREAM, None) .map_err(io_err_into_net_error)?; - socket.set_nonblocking(true).map_err(io_err_into_net_error)?; + socket + .set_nonblocking(true) + .map_err(io_err_into_net_error)?; if addr.is_ipv6() { socket.set_only_v6(only_v6).map_err(io_err_into_net_error)?; } @@ -485,12 +493,21 @@ impl VirtualTcpBoundSocket for LocalTcpBoundSocket { } fn set_ttl(&mut self, ttl: u32) -> Result<()> { - let _ = ttl; - Err(NetworkError::Unsupported) + let socket = self.socket.as_ref().ok_or(NetworkError::InvalidFd)?; + match self.addr_local()?.ip() { + IpAddr::V4(_) => socket.set_ttl_v4(ttl).map_err(io_err_into_net_error), + IpAddr::V6(_) => socket + .set_unicast_hops_v6(ttl) + .map_err(io_err_into_net_error), + } } fn ttl(&self) -> Result { - Err(NetworkError::Unsupported) + let socket = self.socket.as_ref().ok_or(NetworkError::InvalidFd)?; + match self.addr_local()?.ip() { + IpAddr::V4(_) => socket.ttl_v4().map_err(io_err_into_net_error), + IpAddr::V6(_) => socket.unicast_hops_v6().map_err(io_err_into_net_error), + } } } diff --git a/lib/virtual-net/src/loopback.rs b/lib/virtual-net/src/loopback.rs index dfc15cdd7109..d7df98f8dc96 100644 --- a/lib/virtual-net/src/loopback.rs +++ b/lib/virtual-net/src/loopback.rs @@ -1,4 +1,4 @@ -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; use std::net::SocketAddr; use std::sync::Mutex; use std::task::{Context, Poll, Waker}; @@ -7,7 +7,7 @@ use std::{collections::HashMap, sync::Arc}; use crate::tcp_pair::TcpSocketHalf; use crate::{ InterestHandler, IpAddr, IpCidr, Ipv4Addr, Ipv6Addr, NetworkError, VirtualIoSource, - VirtualNetworking, VirtualTcpBoundSocket, VirtualTcpListener, VirtualTcpSocket, + VirtualNetworking, VirtualSocket, VirtualTcpBoundSocket, VirtualTcpListener, VirtualTcpSocket, }; use virtual_mio::InterestType; @@ -17,6 +17,7 @@ const LOOPBACK_EPHEMERAL_PORT_START: u16 = 49152; #[derive(Debug)] struct LoopbackNetworkingState { tcp_listeners: HashMap, + tcp_bound: HashSet, ip_addresses: Vec, next_ephemeral_port: u16, } @@ -25,6 +26,7 @@ impl Default for LoopbackNetworkingState { fn default() -> Self { Self { tcp_listeners: HashMap::new(), + tcp_bound: HashSet::new(), ip_addresses: Vec::new(), next_ephemeral_port: LOOPBACK_EPHEMERAL_PORT_START, } @@ -75,20 +77,29 @@ impl LoopbackNetworking { } } - fn allocate_tcp_bind_addr(state: &mut LoopbackNetworkingState, mut addr: SocketAddr) -> SocketAddr { + fn allocate_tcp_bind_addr( + state: &mut LoopbackNetworkingState, + mut addr: SocketAddr, + ) -> crate::Result { + let is_available = |candidate: SocketAddr, state: &LoopbackNetworkingState| { + let key = Self::normalize_listener_addr(candidate); + !state.tcp_listeners.contains_key(&key) && !state.tcp_bound.contains(&key) + }; + if addr.port() == 0 { let start = state.next_ephemeral_port; let mut candidate = start; loop { let candidate_addr = SocketAddr::new(addr.ip(), candidate); - if !state.tcp_listeners.contains_key(&candidate_addr) { + if is_available(candidate_addr, state) { addr.set_port(candidate); + state.tcp_bound.insert(Self::normalize_listener_addr(addr)); state.next_ephemeral_port = if candidate == u16::MAX { LOOPBACK_EPHEMERAL_PORT_START } else { candidate + 1 }; - break; + return Ok(addr); } candidate = if candidate == u16::MAX { @@ -97,11 +108,19 @@ impl LoopbackNetworking { candidate + 1 }; if candidate == start { - break; + return Err(NetworkError::AddressInUse); } } } - addr + + let reservation_key = Self::normalize_listener_addr(addr); + if state.tcp_listeners.contains_key(&reservation_key) + || state.tcp_bound.contains(&reservation_key) + { + return Err(NetworkError::AddressInUse); + } + state.tcp_bound.insert(reservation_key); + Ok(addr) } fn normalize_listener_addr(mut addr: SocketAddr) -> SocketAddr { @@ -181,18 +200,35 @@ impl VirtualNetworking for LoopbackNetworking { _reuse_addr: bool, ) -> crate::Result> { let mut state = self.state.lock().unwrap(); - let addr = Self::allocate_tcp_bind_addr(&mut state, addr); + let addr = Self::allocate_tcp_bind_addr(&mut state, addr)?; Ok(Box::new(LoopbackTcpBoundSocket { networking: self.clone(), local_addr: addr, + reservation_key: Some(Self::normalize_listener_addr(addr)), + ttl: 64, })) } } +#[cfg(test)] +impl LoopbackNetworking { + pub(crate) fn exhaust_tcp_ephemeral_ports_for_test(&self, ip: IpAddr) { + let mut state = self.state.lock().unwrap(); + for port in LOOPBACK_EPHEMERAL_PORT_START..=u16::MAX { + let addr = SocketAddr::new(ip, port); + state + .tcp_listeners + .insert(addr, LoopbackTcpListener::new(addr, 64)); + } + state.next_ephemeral_port = LOOPBACK_EPHEMERAL_PORT_START; + } +} + #[derive(Debug)] struct LoopbackTcpListenerState { handler: Option>, addr_local: SocketAddr, + ttl: u8, backlog: VecDeque, wakers: Vec, } @@ -203,11 +239,12 @@ pub struct LoopbackTcpListener { } impl LoopbackTcpListener { - pub fn new(addr_local: SocketAddr) -> Self { + pub fn new(addr_local: SocketAddr, ttl: u8) -> Self { Self { state: Arc::new(Mutex::new(LoopbackTcpListenerState { handler: None, addr_local, + ttl, backlog: Default::default(), wakers: Default::default(), })), @@ -216,8 +253,9 @@ impl LoopbackTcpListener { pub fn connect_to(&self, addr_local: SocketAddr) -> TcpSocketHalf { let mut state = self.state.lock().unwrap(); - let (half1, half2) = + let (mut half1, half2) = TcpSocketHalf::channel(DEFAULT_MAX_BUFFER_SIZE, state.addr_local, addr_local); + half1.set_ttl(u32::from(state.ttl)).ok(); state.backlog.push_back(half1); if let Some(handler) = state.handler.as_mut() { @@ -281,19 +319,44 @@ impl VirtualTcpListener for LoopbackTcpListener { Ok(state.addr_local) } - fn set_ttl(&mut self, _ttl: u8) -> crate::Result<()> { + fn set_ttl(&mut self, ttl: u8) -> crate::Result<()> { + let mut state = self.state.lock().unwrap(); + state.ttl = ttl; Ok(()) } fn ttl(&self) -> crate::Result { - Ok(64) + let state = self.state.lock().unwrap(); + Ok(state.ttl) } } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct LoopbackTcpBoundSocket { networking: LoopbackNetworking, local_addr: SocketAddr, + reservation_key: Option, + ttl: u32, +} + +impl LoopbackTcpBoundSocket { + fn release_reservation(&mut self) -> crate::Result<()> { + let reservation_key = self.reservation_key.take().ok_or(NetworkError::InvalidFd)?; + let mut state = self.networking.state.lock().unwrap(); + if !state.tcp_bound.remove(&reservation_key) { + return Err(NetworkError::InvalidFd); + } + Ok(()) + } +} + +impl Drop for LoopbackTcpBoundSocket { + fn drop(&mut self) { + if let Some(reservation_key) = self.reservation_key.take() { + let mut state = self.networking.state.lock().unwrap(); + state.tcp_bound.remove(&reservation_key); + } + } } impl VirtualTcpBoundSocket for LoopbackTcpBoundSocket { @@ -302,28 +365,40 @@ impl VirtualTcpBoundSocket for LoopbackTcpBoundSocket { } fn listen(&mut self) -> crate::Result> { - let listener = LoopbackTcpListener::new(self.local_addr); + let listener = + LoopbackTcpListener::new(self.local_addr, u8::try_from(self.ttl).unwrap_or(u8::MAX)); let mut state = self.networking.state.lock().unwrap(); - state.tcp_listeners.insert( - LoopbackNetworking::normalize_listener_addr(self.local_addr), - listener.clone(), - ); + let reservation_key = self.reservation_key.ok_or(NetworkError::InvalidFd)?; + if !state.tcp_bound.remove(&reservation_key) { + return Err(NetworkError::InvalidFd); + } + if state.tcp_listeners.contains_key(&reservation_key) { + state.tcp_bound.insert(reservation_key); + return Err(NetworkError::AddressInUse); + } + state + .tcp_listeners + .insert(reservation_key, listener.clone()); + self.reservation_key = None; Ok(Box::new(listener)) } fn connect(&mut self, peer: SocketAddr) -> crate::Result> { - let socket = self + let mut socket = self .networking .loopback_connect_to(self.local_addr, peer) .ok_or(NetworkError::ConnectionRefused)?; + self.release_reservation()?; + socket.set_ttl(self.ttl)?; Ok(Box::new(socket)) } - fn set_ttl(&mut self, _ttl: u32) -> crate::Result<()> { - Err(NetworkError::Unsupported) + fn set_ttl(&mut self, ttl: u32) -> crate::Result<()> { + self.ttl = ttl; + Ok(()) } fn ttl(&self) -> crate::Result { - Err(NetworkError::Unsupported) + Ok(self.ttl) } } diff --git a/lib/virtual-net/src/server.rs b/lib/virtual-net/src/server.rs index bae93c2af583..e036e55512f2 100644 --- a/lib/virtual-net/src/server.rs +++ b/lib/virtual-net/src/server.rs @@ -188,6 +188,11 @@ impl RemoteNetworkingServer { let rx = RemoteRx::HyperWebSocket { rx, format }; Self::new(tx, rx, rx_work, inner) } + + #[cfg(test)] + pub(crate) fn socket_count_for_test(&self) -> usize { + self.common.sockets.lock().unwrap().len() + } } #[async_trait::async_trait] @@ -868,14 +873,26 @@ impl RemoteNetworkingServerDriver { socket_id, req_id, ), - RequestType::Close => self.process_inner_noop( - move |socket| match socket { - RemoteAdapterSocket::TcpSocket(s) => s.close(), - _ => Err(NetworkError::Unsupported), - }, - socket_id, - req_id, - ), + RequestType::Close => { + let res = { + let mut guard = self.common.sockets.lock().unwrap(); + self.common.socket_accept.lock().unwrap().remove(&socket_id); + match guard.remove(&socket_id) { + Some(RemoteAdapterSocket::TcpSocket(mut socket)) => socket.close(), + Some(_) => Ok(()), + None => Err(NetworkError::InvalidFd), + } + }; + req_id.and_then(|req_id| { + self.common.send(MessageResponse::ResponseToRequest { + req_id, + res: match res { + Ok(()) => ResponseType::None, + Err(err) => ResponseType::Err(err), + }, + }) + }) + } RequestType::ListenBound => { let res = { let mut guard = self.common.sockets.lock().unwrap(); diff --git a/lib/virtual-net/src/tests.rs b/lib/virtual-net/src/tests.rs index 78cb138966ef..353cc8d3c9f8 100644 --- a/lib/virtual-net/src/tests.rs +++ b/lib/virtual-net/src/tests.rs @@ -149,6 +149,71 @@ async fn test_bound_tcp(client: RemoteNetworkingClient, _server: RemoteNetworkin ); } +#[cfg(feature = "remote")] +async fn test_bound_tcp_ttl(client: RemoteNetworkingClient, _server: RemoteNetworkingServer) { + let mut bound = client + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + bound.set_ttl(42).unwrap(); + assert_eq!( + bound.ttl().unwrap(), + 42, + "remote bound_tcp should round-trip TTL before listen" + ); + + let listener = bound.listen().unwrap(); + assert_eq!( + listener.ttl().unwrap(), + 42, + "remote listener should preserve TTL set while the socket was only bound" + ); +} + +#[cfg(feature = "remote")] +async fn test_bound_tcp_drop_releases_server_socket( + client: RemoteNetworkingClient, + server: RemoteNetworkingServer, +) { + use tokio::time::{Duration, Instant, sleep}; + + let bound = client + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + assert_eq!( + server.socket_count_for_test(), + 1, + "server should retain the bound socket until the client drops it" + ); + + drop(bound); + + let deadline = Instant::now() + Duration::from_secs(1); + loop { + if server.socket_count_for_test() == 0 { + break; + } + assert!( + Instant::now() < deadline, + "server retained a dropped bound tcp socket" + ); + sleep(Duration::from_millis(10)).await; + } +} + #[cfg(feature = "remote")] #[cfg_attr(windows, ignore)] #[traced_test] @@ -169,6 +234,26 @@ async fn test_bound_tcp_with_mpsc() { test_bound_tcp(client, server).await } +#[cfg(feature = "remote")] +#[cfg_attr(windows, ignore)] +#[traced_test] +#[tokio::test(flavor = "multi_thread")] +#[serial_test::serial] +async fn test_bound_tcp_ttl_with_mpsc() { + let (client, server) = setup_mpsc().await; + test_bound_tcp_ttl(client, server).await +} + +#[cfg(feature = "remote")] +#[cfg_attr(windows, ignore)] +#[traced_test] +#[tokio::test(flavor = "multi_thread")] +#[serial_test::serial] +async fn test_bound_tcp_drop_releases_server_socket_with_mpsc() { + let (client, server) = setup_mpsc().await; + test_bound_tcp_drop_releases_server_socket(client, server).await +} + // Disabled on musl due to flakiness. // See https://github.com/wasmerio/wasmer/issues/4425 #[cfg(not(target_env = "musl"))] @@ -593,7 +678,12 @@ async fn test_failed_connect_status_stays_failed() { async fn test_bind_tcp_assigns_ephemeral_port_before_listen() { let networking = LocalNetworking::new(); let mut bound = networking - .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) .await .unwrap(); @@ -622,7 +712,12 @@ async fn test_bind_tcp_keeps_same_port_across_connect() { let networking = LocalNetworking::new(); let mut bound = networking - .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) .await .unwrap(); @@ -637,13 +732,48 @@ async fn test_bind_tcp_keeps_same_port_across_connect() { ); } +#[cfg(not(target_os = "windows"))] +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_bind_tcp_preserves_ttl_across_connect() { + let probe = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); + let peer = probe.local_addr().unwrap(); + + let networking = LocalNetworking::new(); + let mut bound = networking + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + bound.set_ttl(42).unwrap(); + assert_eq!(bound.ttl().unwrap(), 42); + + let socket = bound.connect(peer).unwrap(); + assert_eq!( + socket.ttl().unwrap(), + 42, + "connect should preserve TTL set while the socket was only bound" + ); +} + #[traced_test] #[tokio::test] #[serial_test::serial] async fn test_loopback_bind_tcp_assigns_ephemeral_port_before_listen() { let networking = LoopbackNetworking::new(); let mut bound = networking - .bind_tcp(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), false, false, false) + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) .await .unwrap(); @@ -661,3 +791,150 @@ async fn test_loopback_bind_tcp_assigns_ephemeral_port_before_listen() { "loopback listen should preserve the already-bound local address" ); } + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_preserves_ttl_across_listen() { + let networking = LoopbackNetworking::new(); + let mut bound = networking + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + bound.set_ttl(42).unwrap(); + assert_eq!(bound.ttl().unwrap(), 42); + + let listener = bound.listen().unwrap(); + assert_eq!( + listener.ttl().unwrap(), + 42, + "loopback listen should preserve TTL set while the socket was only bound" + ); +} + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_preserves_ttl_across_connect() { + let server_networking = LoopbackNetworking::new(); + let listener = server_networking + .listen_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + let peer = listener.addr_local().unwrap(); + + let client_networking = server_networking.clone(); + let mut bound = client_networking + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + + bound.set_ttl(42).unwrap(); + assert_eq!(bound.ttl().unwrap(), 42); + + let socket = bound.connect(peer).unwrap(); + assert_eq!( + socket.ttl().unwrap(), + 42, + "loopback connect should preserve TTL set while the socket was only bound" + ); +} + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_returns_error_when_ephemeral_ports_are_exhausted() { + let networking = LoopbackNetworking::new(); + networking.exhaust_tcp_ephemeral_ports_for_test(Ipv4Addr::LOCALHOST.into()); + + let err = networking + .bind_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap_err(); + + assert!( + matches!(err, NetworkError::AddressInUse), + "expected AddressInUse when all loopback ephemeral ports are exhausted, got {err:?}" + ); +} + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_reserves_port_before_listen() { + let networking = LoopbackNetworking::new(); + let bind_addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 40123)); + + let bound = networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap(); + + let err = networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap_err(); + assert!( + matches!(err, NetworkError::AddressInUse), + "expected AddressInUse while a bound socket is reserving the port, got {err:?}" + ); + + drop(bound); + + networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap(); +} + +#[traced_test] +#[tokio::test] +#[serial_test::serial] +async fn test_loopback_bind_tcp_releases_reservation_after_connect() { + let server_networking = LoopbackNetworking::new(); + let listener = server_networking + .listen_tcp( + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + false, + false, + false, + ) + .await + .unwrap(); + let peer = listener.addr_local().unwrap(); + + let client_networking = server_networking.clone(); + let bind_addr = SocketAddr::from((Ipv4Addr::LOCALHOST, 40124)); + let mut bound = client_networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap(); + + let _socket = bound.connect(peer).unwrap(); + + client_networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap(); +} diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index 8ddb8c9f0e8e..b48624fb268f 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -4,7 +4,7 @@ use std::{ mem::MaybeUninit, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, pin::Pin, - sync::{Arc, RwLock, RwLockWriteGuard}, + sync::{Arc, RwLock}, task::{Context, Poll}, time::Duration, }; @@ -265,9 +265,6 @@ impl InodeSocket { // When a sendto or connect call comes in for a UDP "pre-socket", it must be bound to // an ephemeral port automatically. - // Apparently, clippy fails to recognize the write-locked guard being passed into - // the other function, hence the `allow` attribute. - #[allow(clippy::await_holding_lock, clippy::readonly_write_lock)] pub async fn auto_bind_udp( &self, tasks: &dyn VirtualTaskManager, @@ -278,7 +275,7 @@ impl InodeSocket { .ok() .flatten() .unwrap_or(Duration::from_secs(30)); - let inner = self.inner.protected.write().unwrap(); + let inner = self.inner.protected.read().unwrap(); match &inner.kind { InodeSocketKind::PreSocket { props, .. } if props.ty == Socktype::Dgram => { let addr = match props.family { @@ -286,7 +283,8 @@ impl InodeSocket { Addressfamily::Inet6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), _ => return Err(Errno::Notsup), }; - Self::bind_internal(tasks, net, addr, timeout, inner).await + drop(inner); + self.bind_internal(tasks, net, addr, timeout).await } _ => Ok(None), } @@ -303,20 +301,32 @@ impl InodeSocket { .ok() .flatten() .unwrap_or(Duration::from_secs(30)); - let inner = self.inner.protected.write().unwrap(); - Self::bind_internal(tasks, net, set_addr, timeout, inner).await + self.bind_internal(tasks, net, set_addr, timeout).await } - // The lock is dropped before awaiting, but clippy doesn't realize it - #[allow(clippy::await_holding_lock)] async fn bind_internal( + &self, tasks: &dyn VirtualTaskManager, net: &dyn VirtualNetworking, set_addr: SocketAddr, timeout: Duration, - mut inner: RwLockWriteGuard<'_, InodeSocketProtected>, ) -> Result, Errno> { - let socket = { + enum PendingBind { + Tcp { + addr: SocketAddr, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, + }, + Udp { + addr: SocketAddr, + reuse_port: bool, + reuse_addr: bool, + }, + } + + let bind = { + let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, addr, .. } => { match props.family { @@ -345,33 +355,17 @@ impl InodeSocket { let addr = (*addr).unwrap(); match props.ty { - Socktype::Stream => { - let only_v6 = props.only_v6; - let reuse_port = props.reuse_port; - let reuse_addr = props.reuse_addr; - match net.bind_tcp(addr, only_v6, reuse_port, reuse_addr).await { - Ok(socket) => { - let placeholder = SocketProperties::placeholder_from(props); - let props = std::mem::replace(props, placeholder); - return Ok(Some(InodeSocket::new(InodeSocketKind::BoundTcp { - socket, - props, - }))); - } - Err(NetworkError::Unsupported) => { - // Fallback for backends that still only materialize TCP state at - // listen/connect time. - return Ok(None); - } - Err(err) => return Err(net_error_into_wasi_err(err)), - } - } - Socktype::Dgram => { - let reuse_port = props.reuse_port; - let reuse_addr = props.reuse_addr; - - net.bind_udp(addr, reuse_port, reuse_addr) - } + Socktype::Stream => PendingBind::Tcp { + addr, + only_v6: props.only_v6, + reuse_port: props.reuse_port, + reuse_addr: props.reuse_addr, + }, + Socktype::Dgram => PendingBind::Udp { + addr, + reuse_port: props.reuse_port, + reuse_addr: props.reuse_addr, + }, _ => return Err(Errno::Inval), } } @@ -411,12 +405,11 @@ impl InodeSocket { // more to do at this time return Ok(None); } - Socktype::Dgram => { - let reuse_port = props.reuse_port; - let reuse_addr = props.reuse_addr; - - net.bind_udp(addr, reuse_port, reuse_addr) - } + Socktype::Dgram => PendingBind::Udp { + addr, + reuse_port: props.reuse_port, + reuse_addr: props.reuse_addr, + }, _ => return Err(Errno::Inval), } } @@ -425,14 +418,53 @@ impl InodeSocket { } }; - drop(inner); - - tokio::select! { - socket = socket => { - let socket = socket.map_err(net_error_into_wasi_err)?; - Ok(Some(InodeSocket::new(InodeSocketKind::UdpSocket { socket, peer: None }))) - }, - _ = tasks.sleep_now(timeout) => Err(Errno::Timedout) + match bind { + PendingBind::Tcp { + addr, + only_v6, + reuse_port, + reuse_addr, + } => { + tokio::select! { + socket = net.bind_tcp(addr, only_v6, reuse_port, reuse_addr) => { + match socket { + Ok(socket) => { + let props = { + let mut inner = self.inner.protected.write().unwrap(); + match &mut inner.kind { + InodeSocketKind::PreSocket { props, .. } => { + let placeholder = SocketProperties::placeholder_from(props); + std::mem::replace(props, placeholder) + } + _ => return Err(Errno::Inval), + } + }; + Ok(Some(InodeSocket::new(InodeSocketKind::BoundTcp { socket, props }))) + } + Err(NetworkError::Unsupported) => { + // Fallback for backends that still only materialize TCP state at + // listen/connect time. + Ok(None) + } + Err(err) => Err(net_error_into_wasi_err(err)), + } + }, + _ = tasks.sleep_now(timeout) => Err(Errno::Timedout) + } + } + PendingBind::Udp { + addr, + reuse_port, + reuse_addr, + } => { + tokio::select! { + socket = net.bind_udp(addr, reuse_port, reuse_addr) => { + let socket = socket.map_err(net_error_into_wasi_err)?; + Ok(Some(InodeSocket::new(InodeSocketKind::UdpSocket { socket, peer: None }))) + }, + _ = tasks.sleep_now(timeout) => Err(Errno::Timedout) + } + } } } @@ -695,8 +727,7 @@ impl InodeSocket { let no_delay = props.no_delay; let keep_alive = props.keep_alive; let dont_route = props.dont_route; - let mut ret = - socket.connect(peer).map_err(net_error_into_wasi_err)?; + let mut ret = socket.connect(peer).map_err(net_error_into_wasi_err)?; if let Some(no_delay) = no_delay { ret.set_nodelay(no_delay).ok(); } @@ -1723,8 +1754,10 @@ pub(crate) fn all_socket_rights() -> Rights { #[cfg(test)] mod tests { - use super::{InodeSocket, InodeSocketKind, WasiSocketStatus}; + use super::{InodeSocket, InodeSocketKind, SocketProperties, WasiSocketStatus}; + use crate::runtime::task_manager::tokio::TokioTaskManager; use std::{ + future::pending, mem::MaybeUninit, net::{Ipv4Addr, Shutdown, SocketAddr}, pin::Pin, @@ -1738,8 +1771,9 @@ mod tests { use virtual_mio::InterestHandler; use virtual_net::{ NetworkError, Result as NetResult, SocketStatus, VirtualConnectedSocket, VirtualIoSource, - VirtualSocket, VirtualTcpSocket, + VirtualNetworking, VirtualSocket, VirtualTcpBoundSocket, VirtualTcpSocket, }; + use wasmer_wasix_types::wasi::{Addressfamily, Errno, SockProto, Socktype}; #[derive(Debug)] struct MockTcpSocket { @@ -1878,6 +1912,54 @@ mod tests { } } + #[derive(Debug)] + struct MockTcpBoundSocket { + ttl: Arc, + } + + impl VirtualTcpBoundSocket for MockTcpBoundSocket { + fn addr_local(&self) -> NetResult { + Ok(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))) + } + + fn listen(&mut self) -> NetResult> { + Err(NetworkError::Unsupported) + } + + fn connect( + &mut self, + _peer: SocketAddr, + ) -> NetResult> { + Err(NetworkError::Unsupported) + } + + fn set_ttl(&mut self, ttl: u32) -> NetResult<()> { + self.ttl.store(ttl as usize, Ordering::Relaxed); + Ok(()) + } + + fn ttl(&self) -> NetResult { + Ok(self.ttl.load(Ordering::Relaxed) as u32) + } + } + + #[derive(Debug)] + struct PendingBindNetworking; + + #[async_trait::async_trait] + impl VirtualNetworking for PendingBindNetworking { + async fn bind_tcp( + &self, + _addr: SocketAddr, + _only_v6: bool, + _reuse_port: bool, + _reuse_addr: bool, + ) -> NetResult> { + pending::<()>().await; + unreachable!("pending bind_tcp future should never complete") + } + } + #[test] fn inode_socket_poll_write_ready_uses_write_path() { let read_calls = Arc::new(AtomicUsize::new(0)); @@ -1919,4 +2001,72 @@ mod tests { status.store(MOCK_STATUS_OPENED, Ordering::Relaxed); assert!(matches!(inode.status().unwrap(), WasiSocketStatus::Opened)); } + + #[test] + fn inode_socket_bound_tcp_forwards_ttl() { + let ttl = Arc::new(AtomicUsize::new(64)); + let inode = InodeSocket::new(InodeSocketKind::BoundTcp { + socket: Box::new(MockTcpBoundSocket { ttl: ttl.clone() }), + props: SocketProperties { + family: Addressfamily::Inet4, + ty: Socktype::Stream, + pt: SockProto::Tcp, + only_v6: false, + reuse_port: false, + reuse_addr: false, + no_delay: None, + keep_alive: None, + dont_route: None, + send_buf_size: None, + recv_buf_size: None, + write_timeout: None, + read_timeout: None, + accept_timeout: None, + connect_timeout: None, + handler: None, + }, + }); + + inode.set_ttl(42).unwrap(); + assert_eq!(inode.ttl().unwrap(), 42); + assert_eq!(ttl.load(Ordering::Relaxed), 42); + } + + #[tokio::test(flavor = "current_thread")] + async fn inode_socket_tcp_bind_respects_bind_timeout() { + let inode = InodeSocket::new(InodeSocketKind::PreSocket { + props: SocketProperties { + family: Addressfamily::Inet4, + ty: Socktype::Stream, + pt: SockProto::Tcp, + only_v6: false, + reuse_port: false, + reuse_addr: false, + no_delay: None, + keep_alive: None, + dont_route: None, + send_buf_size: None, + recv_buf_size: None, + write_timeout: None, + read_timeout: None, + accept_timeout: None, + connect_timeout: None, + handler: None, + }, + addr: None, + }); + let tasks = TokioTaskManager::default(); + let net = PendingBindNetworking; + + let err = inode + .bind_internal( + &tasks, + &net, + SocketAddr::from((Ipv4Addr::LOCALHOST, 0)), + Duration::from_millis(10), + ) + .await + .unwrap_err(); + assert_eq!(err, Errno::Timedout); + } } diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c index 742ce91e2518..4bf441db4415 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-port-zero-connect/main.c @@ -27,7 +27,8 @@ int main(void) { return 1; } - if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { + if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < + 0) { perror("bind(server)"); close(server_fd); return 1; @@ -64,7 +65,8 @@ int main(void) { return 1; } - if (bind(client_fd, (struct sockaddr*)&client_bind_addr, sizeof(client_bind_addr)) < 0) { + if (bind(client_fd, (struct sockaddr*)&client_bind_addr, + sizeof(client_bind_addr)) < 0) { perror("bind(client)"); close(client_fd); close(server_fd); @@ -87,7 +89,8 @@ int main(void) { return 1; } - if (connect(client_fd, (struct sockaddr*)&server_bound_addr, sizeof(server_bound_addr)) < 0) { + if (connect(client_fd, (struct sockaddr*)&server_bound_addr, + sizeof(server_bound_addr)) < 0) { perror("connect(client)"); close(client_fd); close(server_fd); @@ -104,9 +107,10 @@ int main(void) { int connect_port = ntohs(client_after_connect.sin_port); if (connect_port != bind_port) { - fprintf(stderr, - "expected client port to stay stable across connect, got %d then %d\n", - bind_port, connect_port); + fprintf( + stderr, + "expected client port to stay stable across connect, got %d then %d\n", + bind_port, connect_port); close(client_fd); close(server_fd); return 1; From 263e767b2e865aa5dc8f582cf092af8953971e49 Mon Sep 17 00:00:00 2001 From: Arshia Date: Wed, 15 Apr 2026 18:07:37 +0400 Subject: [PATCH 04/13] Upgrade rustls-webpki --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 608b0be16949..24e78823015b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4790,9 +4790,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", From 289f448f336dc149538f308130a3ee83c77a8984 Mon Sep 17 00:00:00 2001 From: Arshia Date: Wed, 15 Apr 2026 18:24:06 +0400 Subject: [PATCH 05/13] Make clippy happy --- lib/virtual-net/src/host.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index ad02868f493d..c161306d689c 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -88,10 +88,10 @@ fn tcp_connect_in_progress(err: &io::Error) -> bool { #[cfg(all(target_family = "unix", feature = "libc"))] { - return matches!( + matches!( err.raw_os_error(), Some(raw) if raw == libc::EINPROGRESS || raw == libc::EALREADY - ); + ) } #[cfg(not(all(target_family = "unix", feature = "libc")))] @@ -475,10 +475,10 @@ impl VirtualTcpBoundSocket for LocalTcpBoundSocket { } let socket = self.socket.take().ok_or(NetworkError::InvalidFd)?; - if let Err(err) = socket.connect(&peer.into()) { - if !tcp_connect_in_progress(&err) { - return Err(io_err_into_net_error(err)); - } + if let Err(err) = socket.connect(&peer.into()) + && !tcp_connect_in_progress(&err) + { + return Err(io_err_into_net_error(err)); } let stream = mio::net::TcpStream::from_std(socket.into()); From 4ca1d2a19c45dddfc40fbab9f962046ab0d6009d Mon Sep 17 00:00:00 2001 From: Arshia Ghafoori Date: Wed, 15 Apr 2026 15:43:13 +0000 Subject: [PATCH 06/13] fix: address lint issues in tcp bind changes --- lib/virtual-net/src/host.rs | 26 ++++++++++++------------- lib/wasix/src/net/socket.rs | 38 ++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index c161306d689c..05e667b65767 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -83,20 +83,20 @@ fn tcp_connect_in_progress(err: &io::Error) -> bool { err.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted ) { - return true; - } - - #[cfg(all(target_family = "unix", feature = "libc"))] - { - matches!( - err.raw_os_error(), - Some(raw) if raw == libc::EINPROGRESS || raw == libc::EALREADY - ) - } + true + } else { + #[cfg(all(target_family = "unix", feature = "libc"))] + { + matches!( + err.raw_os_error(), + Some(raw) if raw == libc::EINPROGRESS || raw == libc::EALREADY + ) + } - #[cfg(not(all(target_family = "unix", feature = "libc")))] - { - false + #[cfg(not(all(target_family = "unix", feature = "libc")))] + { + false + } } } diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index b48624fb268f..8bdf9048ffc9 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -32,6 +32,9 @@ pub enum InodeHttpSocketType { Headers, } +type TcpConnectFuture<'a> = + Pin, Errno>> + 'a>>; + #[derive(Debug)] pub struct SocketProperties { pub family: Addressfamily, @@ -275,19 +278,26 @@ impl InodeSocket { .ok() .flatten() .unwrap_or(Duration::from_secs(30)); - let inner = self.inner.protected.read().unwrap(); - match &inner.kind { - InodeSocketKind::PreSocket { props, .. } if props.ty == Socktype::Dgram => { - let addr = match props.family { - Addressfamily::Inet4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), - Addressfamily::Inet6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), - _ => return Err(Errno::Notsup), - }; - drop(inner); - self.bind_internal(tasks, net, addr, timeout).await + let family = { + let inner = self.inner.protected.read().unwrap(); + match &inner.kind { + InodeSocketKind::PreSocket { props, .. } if props.ty == Socktype::Dgram => { + Some(props.family) + } + _ => None, } - _ => Ok(None), - } + }; + let Some(family) = family else { + return Ok(None); + }; + + let addr = match family { + Addressfamily::Inet4 => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + Addressfamily::Inet6 => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0), + _ => return Err(Errno::Notsup), + }; + + self.bind_internal(tasks, net, addr, timeout).await } pub async fn bind( @@ -668,9 +678,7 @@ impl InodeSocket { let timeout = timeout.unwrap_or(Duration::from_secs(30)); let handler; - let connect: Pin< - Box, Errno>> + '_>, - > = { + let connect: TcpConnectFuture<'_> = { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, addr, .. } => { From 1b1276353ed7dd89ef80cd7094e79aaa8e4dacba Mon Sep 17 00:00:00 2001 From: Arshia Date: Wed, 15 Apr 2026 19:49:12 +0400 Subject: [PATCH 07/13] Remove the plan doc --- docs/dev/issue-6403-tcp-bind-plan.md | 129 --------------------------- 1 file changed, 129 deletions(-) delete mode 100644 docs/dev/issue-6403-tcp-bind-plan.md diff --git a/docs/dev/issue-6403-tcp-bind-plan.md b/docs/dev/issue-6403-tcp-bind-plan.md deleted file mode 100644 index a2499af2061a..000000000000 --- a/docs/dev/issue-6403-tcp-bind-plan.md +++ /dev/null @@ -1,129 +0,0 @@ -# Issue #6403 Plan: Allocate TCP Ephemeral Ports at `bind()` - -Issue: [wasmerio/wasmer#6403](https://github.com/wasmerio/wasmer/issues/6403) - -## Problem Summary - -For TCP sockets in Wasix, `bind(("host", 0))` currently stores the requested address but does not perform a real backend bind. As a result: - -- `getsockname()` after `bind()` still reports port `0` -- the kernel-assigned ephemeral port only appears after `listen()` -- code that relies on POSIX behavior breaks - -Native systems allocate the ephemeral port at `bind()` time, not at `listen()` time. - -## Root Cause - -The relevant behavior is in `lib/wasix/src/net/socket.rs`. - -- `InodeSocket::bind()` stores the requested TCP address for `PreSocket` / `RemoteSocket` stream sockets and returns without calling into the networking backend. -- `InodeSocket::addr_local()` for those pre-listen socket states simply returns the stored address, which remains `host:0`. -- `InodeSocket::listen()` is the first place that actually calls `net.listen_tcp(...)`, so the real OS bind and ephemeral port allocation happen too late. - -This is not just a `getsockname()` reporting bug. The underlying port is not actually reserved until `listen()`. - -## Secondary Constraint - -The current `virtual-net` abstraction exposes: - -- `listen_tcp(...)` -- `bind_udp(...)` -- `connect_tcp(...)` - -but it does not expose a TCP bind primitive that can: - -- perform a real bind without listening yet -- report the effective local address after binding -- later transition into `listen()` or `connect()` - -That means the fix needs to extend the backend abstraction rather than only patching Wasix-local state. - -## Proposed Fix - -### 1. Add a regression test first - -Add a new socket test under `lib/wasix/tests/wasm_tests/socket_tests/` that: - -1. creates an IPv4 TCP socket -2. binds to `127.0.0.1:0` -3. checks that `getsockname().port != 0` immediately after `bind()` -4. calls `listen()` -5. checks that the port stays the same after `listen()` - -This locks in the POSIX behavior expected by the issue report. - -## 2. Introduce a real TCP-bound socket state in `virtual-net` - -Extend `lib/virtual-net` with a TCP bind API and a corresponding bound-socket type that can: - -- return `addr_local()` -- transition into a TCP listener -- transition into a TCP stream connection - -At a minimum, the new backend capability needs to preserve the actual local port selected during `bind()`. - -## 3. Implement the new backend path - -### Host backend - -Update `lib/virtual-net/src/host.rs` to create a TCP socket explicitly, apply socket options, call `bind()`, and read back the effective local address before any later `listen()` or `connect()` step. - -This likely requires `socket2`, similar to the existing UDP bind implementation. - -### Loopback backend - -Update `lib/virtual-net/src/loopback.rs` so a TCP bind to port `0` allocates an ephemeral port during bind, rather than preserving `0` until listen. - -### Remote client/server backend - -Update `lib/virtual-net/src/meta.rs`, `client.rs`, and `server.rs` to carry the new TCP bind operation across the remote networking protocol. - -Without this, Wasix behavior will diverge depending on which backend is active. - -## 4. Update the Wasix socket state machine - -In `lib/wasix/src/net/socket.rs`: - -- make TCP `bind()` return a real upgraded socket object instead of `Ok(None)` -- add a socket state representing “TCP socket bound locally but not yet listening/connected” -- make `addr_local()` read the effective address from that bound socket state -- make `listen()` consume the bound socket instead of rebinding from scratch -- make `connect()` also honor the previously bound local address - -This keeps bind/listen/connect semantics aligned and avoids reporting a port that is not actually reserved. - -## 5. Fix journaling semantics - -`lib/wasix/src/syscalls/wasix/sock_bind.rs` currently journals the requested address from guest memory, which is wrong for `bind(port=0)`. - -After the functional fix: - -- query the effective local address after `sock_bind` succeeds -- journal that effective address instead of the requested `host:0` - -Otherwise journal replay can observe a different port from the one the program originally saw. - -## Implementation Order - -1. Add the Wasix regression test for `bind(..., 0)` + `getsockname()`. -2. Add the new TCP bind abstraction in `virtual-net`. -3. Implement the host backend first. -4. Update Wasix socket state transitions to use the real bound socket. -5. Update journaling to store the effective address. -6. Extend loopback and remote client/server backends. -7. Run targeted socket tests and any relevant `virtual-net` tests. - -## Non-Goals - -- Faking `getsockname()` by inventing a port in Wasix state without actually reserving it -- Fixing only the listen path while leaving bind-then-connect semantics inconsistent -- Fixing only the host backend and leaving other `virtual-net` backends with different behavior - -## Expected Outcome - -After the fix: - -- `bind(("127.0.0.1", 0))` allocates a real ephemeral port immediately -- `getsockname()` reports the assigned port right after `bind()` -- `listen()` keeps the same local port -- journal replay preserves the same observed bound address From 7eda7f321bcc66b8a3e6ceeede0730527280d256 Mon Sep 17 00:00:00 2001 From: Arshia Date: Fri, 17 Apr 2026 09:58:40 +0400 Subject: [PATCH 08/13] Get non-unix platforms happy too --- lib/virtual-net/src/host.rs | 1 + lib/wasix/src/net/socket.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index 05e667b65767..31bee8dda94a 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -78,6 +78,7 @@ fn tcp_socket_domain(addr: SocketAddr) -> socket2::Domain { } } +#[allow(clippy::needless_bool)] fn tcp_connect_in_progress(err: &io::Error) -> bool { if matches!( err.kind(), diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index 8bdf9048ffc9..7ddf6b9b8023 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -1763,7 +1763,6 @@ pub(crate) fn all_socket_rights() -> Rights { #[cfg(test)] mod tests { use super::{InodeSocket, InodeSocketKind, SocketProperties, WasiSocketStatus}; - use crate::runtime::task_manager::tokio::TokioTaskManager; use std::{ future::pending, mem::MaybeUninit, @@ -2040,6 +2039,7 @@ mod tests { assert_eq!(ttl.load(Ordering::Relaxed), 42); } + #[cfg(feature = "sys")] #[tokio::test(flavor = "current_thread")] async fn inode_socket_tcp_bind_respects_bind_timeout() { let inode = InodeSocket::new(InodeSocketKind::PreSocket { @@ -2063,7 +2063,7 @@ mod tests { }, addr: None, }); - let tasks = TokioTaskManager::default(); + let tasks = crate::runtime::task_manager::tokio::TokioTaskManager::default(); let net = PendingBindNetworking; let err = inode From 5cc98ceed1344841cd9dfbc5c3363ec807efe602 Mon Sep 17 00:00:00 2001 From: Arshia Date: Thu, 23 Apr 2026 15:38:24 +0400 Subject: [PATCH 09/13] Address review comments --- lib/virtual-net/src/host.rs | 12 +++++++++++- lib/virtual-net/src/loopback.rs | 24 ++++++------------------ lib/wasix/src/net/socket.rs | 13 ++++++------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/virtual-net/src/host.rs b/lib/virtual-net/src/host.rs index 31bee8dda94a..4549a1a8d95e 100644 --- a/lib/virtual-net/src/host.rs +++ b/lib/virtual-net/src/host.rs @@ -29,6 +29,14 @@ use virtual_mio::{ HandlerGuardState, InterestGuard, InterestHandler, InterestType, Selector, state_as_waker_map, }; +/// Use the platform's maximum listen backlog where available so that +/// `LocalTcpBoundSocket::listen` preserves the same accept capacity as +/// the previous `std::net::TcpListener`-based implementation. +#[cfg(all(target_family = "unix", feature = "libc"))] +const LISTEN_BACKLOG: i32 = libc::SOMAXCONN; +#[cfg(not(all(target_family = "unix", feature = "libc")))] +const LISTEN_BACKLOG: i32 = 128; + #[derive(Debug)] pub struct LocalNetworking { selector: Arc, @@ -454,7 +462,9 @@ impl VirtualTcpBoundSocket for LocalTcpBoundSocket { fn listen(&mut self) -> Result> { let socket = self.socket.take().ok_or(NetworkError::InvalidFd)?; - socket.listen(128).map_err(io_err_into_net_error)?; + socket + .listen(LISTEN_BACKLOG) + .map_err(io_err_into_net_error)?; let listener = mio::net::TcpListener::from_std(socket.into()); Ok(Box::new(LocalTcpListener { stream: listener, diff --git a/lib/virtual-net/src/loopback.rs b/lib/virtual-net/src/loopback.rs index d7df98f8dc96..7047a89dcae4 100644 --- a/lib/virtual-net/src/loopback.rs +++ b/lib/virtual-net/src/loopback.rs @@ -185,11 +185,13 @@ impl VirtualNetworking for LoopbackNetworking { async fn listen_tcp( &self, addr: SocketAddr, - _only_v6: bool, - _reuse_port: bool, - _reuse_addr: bool, + only_v6: bool, + reuse_port: bool, + reuse_addr: bool, ) -> crate::Result> { - self.bind_tcp(addr, false, false, false).await?.listen() + self.bind_tcp(addr, only_v6, reuse_port, reuse_addr) + .await? + .listen() } async fn bind_tcp( @@ -210,20 +212,6 @@ impl VirtualNetworking for LoopbackNetworking { } } -#[cfg(test)] -impl LoopbackNetworking { - pub(crate) fn exhaust_tcp_ephemeral_ports_for_test(&self, ip: IpAddr) { - let mut state = self.state.lock().unwrap(); - for port in LOOPBACK_EPHEMERAL_PORT_START..=u16::MAX { - let addr = SocketAddr::new(ip, port); - state - .tcp_listeners - .insert(addr, LoopbackTcpListener::new(addr, 64)); - } - state.next_ephemeral_port = LOOPBACK_EPHEMERAL_PORT_START; - } -} - #[derive(Debug)] struct LoopbackTcpListenerState { handler: Option>, diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index 7ddf6b9b8023..dd6e5375515a 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -55,12 +55,12 @@ pub struct SocketProperties { pub handler: Option>, } -impl SocketProperties { - fn placeholder_from(existing: &Self) -> Self { +impl Default for SocketProperties { + fn default() -> Self { Self { - family: existing.family, - ty: existing.ty, - pt: existing.pt, + family: Addressfamily::Unspec, + ty: Socktype::Unknown, + pt: SockProto::Ip, only_v6: false, reuse_port: false, reuse_addr: false, @@ -443,8 +443,7 @@ impl InodeSocket { let mut inner = self.inner.protected.write().unwrap(); match &mut inner.kind { InodeSocketKind::PreSocket { props, .. } => { - let placeholder = SocketProperties::placeholder_from(props); - std::mem::replace(props, placeholder) + std::mem::take(props) } _ => return Err(Errno::Inval), } From ad07dbedd9d7a6fbca2ff682d74f03d7afe1d68e Mon Sep 17 00:00:00 2001 From: Arshia Date: Thu, 23 Apr 2026 15:39:11 +0400 Subject: [PATCH 10/13] oopsie --- lib/virtual-net/src/loopback.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/virtual-net/src/loopback.rs b/lib/virtual-net/src/loopback.rs index 7047a89dcae4..647849243828 100644 --- a/lib/virtual-net/src/loopback.rs +++ b/lib/virtual-net/src/loopback.rs @@ -212,6 +212,20 @@ impl VirtualNetworking for LoopbackNetworking { } } +#[cfg(test)] +impl LoopbackNetworking { + pub(crate) fn exhaust_tcp_ephemeral_ports_for_test(&self, ip: IpAddr) { + let mut state = self.state.lock().unwrap(); + for port in LOOPBACK_EPHEMERAL_PORT_START..=u16::MAX { + let addr = SocketAddr::new(ip, port); + state + .tcp_listeners + .insert(addr, LoopbackTcpListener::new(addr, 64)); + } + state.next_ephemeral_port = LOOPBACK_EPHEMERAL_PORT_START; + } +} + #[derive(Debug)] struct LoopbackTcpListenerState { handler: Option>, From 2796860a266f2c2a6fb5b03999ec79dd439ad567 Mon Sep 17 00:00:00 2001 From: Arshia Date: Thu, 23 Apr 2026 16:51:33 +0400 Subject: [PATCH 11/13] Address review comments --- lib/wasix/src/net/socket.rs | 20 +++- lib/wasix/tests/wasm_tests/socket_tests.rs | 15 +++ .../bind-fail-stays-unbound/build.sh | 3 + .../bind-fail-stays-unbound/main.c | 109 ++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) create mode 100755 lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/build.sh create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/main.c diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index dd6e5375515a..7c954b0acea4 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -455,10 +455,26 @@ impl InodeSocket { // listen/connect time. Ok(None) } - Err(err) => Err(net_error_into_wasi_err(err)), + Err(err) => { + // Roll back the pre-set address so the socket stays unbound, + // matching Linux semantics where a failed bind(2) leaves the + // socket unbound. + let mut inner = self.inner.protected.write().unwrap(); + if let InodeSocketKind::PreSocket { addr, .. } = &mut inner.kind { + addr.take(); + } + Err(net_error_into_wasi_err(err)) + } } }, - _ = tasks.sleep_now(timeout) => Err(Errno::Timedout) + _ = tasks.sleep_now(timeout) => { + // Bind timed out; roll back the pre-set address for the same reason. + let mut inner = self.inner.protected.write().unwrap(); + if let InodeSocketKind::PreSocket { addr, .. } = &mut inner.kind { + addr.take(); + } + Err(Errno::Timedout) + } } } PendingBind::Udp { diff --git a/lib/wasix/tests/wasm_tests/socket_tests.rs b/lib/wasix/tests/wasm_tests/socket_tests.rs index 0173d1cfd4b4..bd55c9ed9a02 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests.rs +++ b/lib/wasix/tests/wasm_tests/socket_tests.rs @@ -62,6 +62,21 @@ fn test_bind_port_zero_keeps_same_port_across_connect() { ); } +#[test] +fn test_bind_fail_leaves_socket_unbound() { + let wasm = run_build_script(file!(), "bind-fail-stays-unbound").unwrap(); + let result = run_wasm_with_result(&wasm, wasm.parent().unwrap()).unwrap(); + let stdout = String::from_utf8_lossy(&result.stdout); + assert_eq!( + stdout.trim(), + "bind failure leaves socket unbound", + "exit_code={:?}\nstdout:\n{}\nstderr:\n{}", + result.exit_code, + stdout, + String::from_utf8_lossy(&result.stderr) + ); +} + #[test] // https://github.com/wasmerio/wasmer/issues/6366 #[ignore = "flaky test (#6366)"] diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/build.sh b/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/build.sh new file mode 100755 index 000000000000..b3d2a483d5fd --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +$CC main.c -o main diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/main.c b/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/main.c new file mode 100644 index 000000000000..4f61c679762b --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bind-fail-stays-unbound/main.c @@ -0,0 +1,109 @@ +/* + * Verify that a failed bind(2) leaves the socket logically unbound. + * + * Steps: + * 1. Bind socket A to 127.0.0.1:0 so the OS assigns an ephemeral port. + * 2. Read the assigned port back with getsockname. + * 3. Try to bind socket B to the same address/port — this must fail with + * EADDRINUSE because socket A still holds the port. + * 4. Call getsockname on socket B. The returned port must be 0, proving + * the failed bind did not leave a stale address on the socket. + */ +#include +#include +#include +#include +#include +#include + +int main(void) { + /* --- socket A: claim an ephemeral port --- */ + int fd_a = socket(AF_INET, SOCK_STREAM, 0); + if (fd_a < 0) { + perror("socket A"); + return 1; + } + + struct sockaddr_in addr_zero; + memset(&addr_zero, 0, sizeof(addr_zero)); + addr_zero.sin_family = AF_INET; + addr_zero.sin_port = htons(0); + if (inet_pton(AF_INET, "127.0.0.1", &addr_zero.sin_addr) != 1) { + fprintf(stderr, "inet_pton failed\n"); + close(fd_a); + return 1; + } + + if (bind(fd_a, (struct sockaddr*)&addr_zero, sizeof(addr_zero)) < 0) { + perror("bind A"); + close(fd_a); + return 1; + } + + /* Find out which port was assigned to socket A. */ + struct sockaddr_in addr_a; + socklen_t len = sizeof(addr_a); + memset(&addr_a, 0, sizeof(addr_a)); + if (getsockname(fd_a, (struct sockaddr*)&addr_a, &len) < 0) { + perror("getsockname A"); + close(fd_a); + return 1; + } + + int port_a = (int)ntohs(addr_a.sin_port); + if (port_a == 0) { + fprintf(stderr, "getsockname returned port 0 for socket A\n"); + close(fd_a); + return 1; + } + + /* --- socket B: attempt a conflicting bind --- */ + int fd_b = socket(AF_INET, SOCK_STREAM, 0); + if (fd_b < 0) { + perror("socket B"); + close(fd_a); + return 1; + } + + /* Bind B to the exact same address that A already owns. */ + if (bind(fd_b, (struct sockaddr*)&addr_a, sizeof(addr_a)) == 0) { + fprintf(stderr, "bind B unexpectedly succeeded on port %d\n", port_a); + close(fd_a); + close(fd_b); + return 1; + } + if (errno != EADDRINUSE) { + fprintf(stderr, "bind B failed with errno %d (%s), expected EADDRINUSE\n", + errno, strerror(errno)); + close(fd_a); + close(fd_b); + return 1; + } + + /* --- check that socket B is still unbound --- */ + struct sockaddr_in local_b; + socklen_t len_b = sizeof(local_b); + memset(&local_b, 0, sizeof(local_b)); + if (getsockname(fd_b, (struct sockaddr*)&local_b, &len_b) < 0) { + perror("getsockname B"); + close(fd_a); + close(fd_b); + return 1; + } + + int port_b = (int)ntohs(local_b.sin_port); + if (port_b != 0) { + fprintf(stderr, + "after failed bind, getsockname returned port %d for socket B " + "(expected 0 — socket should still be unbound)\n", + port_b); + close(fd_a); + close(fd_b); + return 1; + } + + close(fd_a); + close(fd_b); + printf("bind failure leaves socket unbound\n"); + return 0; +} From 70e9b786ee104c129731b093f005d6ac11c2e52b Mon Sep 17 00:00:00 2001 From: Arshia Date: Thu, 23 Apr 2026 18:25:54 +0400 Subject: [PATCH 12/13] Address more review comments --- lib/virtual-net/src/loopback.rs | 178 ++++++++++++++++-- lib/virtual-net/src/tests.rs | 16 +- lib/wasix/src/net/socket.rs | 4 +- lib/wasix/tests/wasm_tests/socket_tests.rs | 15 ++ .../socket_tests/bound-tcp-writable/build.sh | 3 + .../socket_tests/bound-tcp-writable/main.c | 68 +++++++ .../connect-holds-local-port/build.sh | 3 + .../connect-holds-local-port/main.c | 157 +++++++++++++++ 8 files changed, 426 insertions(+), 18 deletions(-) create mode 100755 lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/build.sh create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/main.c create mode 100755 lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/build.sh create mode 100644 lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c diff --git a/lib/virtual-net/src/loopback.rs b/lib/virtual-net/src/loopback.rs index 647849243828..23f43ad25cb6 100644 --- a/lib/virtual-net/src/loopback.rs +++ b/lib/virtual-net/src/loopback.rs @@ -6,8 +6,9 @@ use std::{collections::HashMap, sync::Arc}; use crate::tcp_pair::TcpSocketHalf; use crate::{ - InterestHandler, IpAddr, IpCidr, Ipv4Addr, Ipv6Addr, NetworkError, VirtualIoSource, - VirtualNetworking, VirtualSocket, VirtualTcpBoundSocket, VirtualTcpListener, VirtualTcpSocket, + InterestHandler, IpAddr, IpCidr, Ipv4Addr, Ipv6Addr, NetworkError, VirtualConnectedSocket, + VirtualIoSource, VirtualNetworking, VirtualSocket, VirtualTcpBoundSocket, VirtualTcpListener, + VirtualTcpSocket, }; use virtual_mio::InterestType; @@ -226,6 +227,156 @@ impl LoopbackNetworking { } } +/// A connected TCP socket that keeps its local-port reservation in +/// `LoopbackNetworkingState::tcp_bound` until it is explicitly closed or +/// dropped, matching POSIX/Linux semantics where a connected socket holds +/// its local port for its entire lifetime. +#[derive(Debug)] +struct LoopbackConnectedSocket { + inner: TcpSocketHalf, + networking: LoopbackNetworking, + /// `None` once the reservation has been released (after `close()` or `drop`). + reservation_key: Option, +} + +impl LoopbackConnectedSocket { + fn release_reservation(&mut self) { + if let Some(key) = self.reservation_key.take() { + self.networking.state.lock().unwrap().tcp_bound.remove(&key); + } + } +} + +impl Drop for LoopbackConnectedSocket { + fn drop(&mut self) { + self.release_reservation(); + } +} + +impl VirtualIoSource for LoopbackConnectedSocket { + fn remove_handler(&mut self) { + self.inner.remove_handler(); + } + + fn poll_read_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_read_ready(cx) + } + + fn poll_write_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_write_ready(cx) + } +} + +impl VirtualSocket for LoopbackConnectedSocket { + fn set_ttl(&mut self, ttl: u32) -> crate::Result<()> { + self.inner.set_ttl(ttl) + } + + fn ttl(&self) -> crate::Result { + self.inner.ttl() + } + + fn addr_local(&self) -> crate::Result { + self.inner.addr_local() + } + + fn status(&self) -> crate::Result { + self.inner.status() + } + + fn set_handler( + &mut self, + handler: Box, + ) -> crate::Result<()> { + self.inner.set_handler(handler) + } +} + +impl VirtualConnectedSocket for LoopbackConnectedSocket { + fn set_linger(&mut self, linger: Option) -> crate::Result<()> { + self.inner.set_linger(linger) + } + + fn linger(&self) -> crate::Result> { + self.inner.linger() + } + + fn try_send(&mut self, data: &[u8]) -> crate::Result { + self.inner.try_send(data) + } + + fn try_flush(&mut self) -> crate::Result<()> { + self.inner.try_flush() + } + + fn close(&mut self) -> crate::Result<()> { + self.release_reservation(); + self.inner.close() + } + + fn try_recv( + &mut self, + buf: &mut [std::mem::MaybeUninit], + peek: bool, + ) -> crate::Result { + self.inner.try_recv(buf, peek) + } +} + +impl VirtualTcpSocket for LoopbackConnectedSocket { + fn set_recv_buf_size(&mut self, size: usize) -> crate::Result<()> { + self.inner.set_recv_buf_size(size) + } + + fn recv_buf_size(&self) -> crate::Result { + self.inner.recv_buf_size() + } + + fn set_send_buf_size(&mut self, size: usize) -> crate::Result<()> { + self.inner.set_send_buf_size(size) + } + + fn send_buf_size(&self) -> crate::Result { + self.inner.send_buf_size() + } + + fn set_nodelay(&mut self, reuse: bool) -> crate::Result<()> { + self.inner.set_nodelay(reuse) + } + + fn nodelay(&self) -> crate::Result { + self.inner.nodelay() + } + + fn set_keepalive(&mut self, keepalive: bool) -> crate::Result<()> { + self.inner.set_keepalive(keepalive) + } + + fn keepalive(&self) -> crate::Result { + self.inner.keepalive() + } + + fn set_dontroute(&mut self, dontroute: bool) -> crate::Result<()> { + self.inner.set_dontroute(dontroute) + } + + fn dontroute(&self) -> crate::Result { + self.inner.dontroute() + } + + fn addr_peer(&self) -> crate::Result { + self.inner.addr_peer() + } + + fn shutdown(&mut self, how: std::net::Shutdown) -> crate::Result<()> { + self.inner.shutdown(how) + } + + fn is_closed(&self) -> bool { + self.inner.is_closed() + } +} + #[derive(Debug)] struct LoopbackTcpListenerState { handler: Option>, @@ -341,17 +492,6 @@ pub struct LoopbackTcpBoundSocket { ttl: u32, } -impl LoopbackTcpBoundSocket { - fn release_reservation(&mut self) -> crate::Result<()> { - let reservation_key = self.reservation_key.take().ok_or(NetworkError::InvalidFd)?; - let mut state = self.networking.state.lock().unwrap(); - if !state.tcp_bound.remove(&reservation_key) { - return Err(NetworkError::InvalidFd); - } - Ok(()) - } -} - impl Drop for LoopbackTcpBoundSocket { fn drop(&mut self) { if let Some(reservation_key) = self.reservation_key.take() { @@ -390,9 +530,17 @@ impl VirtualTcpBoundSocket for LoopbackTcpBoundSocket { .networking .loopback_connect_to(self.local_addr, peer) .ok_or(NetworkError::ConnectionRefused)?; - self.release_reservation()?; + // Transfer the port reservation to the connected socket so that the + // local port stays in `tcp_bound` for the socket's entire lifetime, + // matching POSIX/Linux semantics (a connected socket holds its local + // port; rebinding it returns EADDRINUSE). + let reservation_key = self.reservation_key.take().ok_or(NetworkError::InvalidFd)?; socket.set_ttl(self.ttl)?; - Ok(Box::new(socket)) + Ok(Box::new(LoopbackConnectedSocket { + inner: socket, + networking: self.networking.clone(), + reservation_key: Some(reservation_key), + })) } fn set_ttl(&mut self, ttl: u32) -> crate::Result<()> { diff --git a/lib/virtual-net/src/tests.rs b/lib/virtual-net/src/tests.rs index 353cc8d3c9f8..d75ae6303480 100644 --- a/lib/virtual-net/src/tests.rs +++ b/lib/virtual-net/src/tests.rs @@ -911,7 +911,7 @@ async fn test_loopback_bind_tcp_reserves_port_before_listen() { #[traced_test] #[tokio::test] #[serial_test::serial] -async fn test_loopback_bind_tcp_releases_reservation_after_connect() { +async fn test_loopback_connected_socket_holds_local_port_reservation() { let server_networking = LoopbackNetworking::new(); let listener = server_networking .listen_tcp( @@ -931,8 +931,20 @@ async fn test_loopback_bind_tcp_releases_reservation_after_connect() { .await .unwrap(); - let _socket = bound.connect(peer).unwrap(); + let socket = bound.connect(peer).unwrap(); + + // While the connected socket is alive the local port must stay reserved. + let err = client_networking + .bind_tcp(bind_addr, false, false, false) + .await + .unwrap_err(); + assert!( + matches!(err, NetworkError::AddressInUse), + "expected AddressInUse while connected socket holds the port, got {err:?}" + ); + // After the connected socket is dropped the port must be released. + drop(socket); client_networking .bind_tcp(bind_addr, false, false, false) .await diff --git a/lib/wasix/src/net/socket.rs b/lib/wasix/src/net/socket.rs index 7c954b0acea4..7045d132c770 100644 --- a/lib/wasix/src/net/socket.rs +++ b/lib/wasix/src/net/socket.rs @@ -1724,7 +1724,9 @@ impl InodeSocketProtected { InodeSocketKind::UdpSocket { socket, .. } => socket.poll_write_ready(cx), InodeSocketKind::Raw(socket) => socket.poll_write_ready(cx), InodeSocketKind::Icmp(socket) => socket.poll_write_ready(cx), - InodeSocketKind::BoundTcp { .. } => Poll::Pending, + // A bound-but-not-yet-listening TCP socket is writable immediately, + // matching Linux select()/poll() semantics. + InodeSocketKind::BoundTcp { .. } => Poll::Ready(Ok(0)), InodeSocketKind::PreSocket { .. } => Poll::Pending, InodeSocketKind::RemoteSocket { is_dead, .. } => match is_dead { true => Poll::Ready(Ok(0)), diff --git a/lib/wasix/tests/wasm_tests/socket_tests.rs b/lib/wasix/tests/wasm_tests/socket_tests.rs index bd55c9ed9a02..500440a8f8bf 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests.rs +++ b/lib/wasix/tests/wasm_tests/socket_tests.rs @@ -62,6 +62,21 @@ fn test_bind_port_zero_keeps_same_port_across_connect() { ); } +#[test] +fn test_bound_tcp_socket_is_writable() { + let wasm = run_build_script(file!(), "bound-tcp-writable").unwrap(); + let result = run_wasm_with_result(&wasm, wasm.parent().unwrap()).unwrap(); + let stdout = String::from_utf8_lossy(&result.stdout); + assert_eq!( + stdout.trim(), + "bound TCP socket is writable", + "exit_code={:?}\nstdout:\n{}\nstderr:\n{}", + result.exit_code, + stdout, + String::from_utf8_lossy(&result.stderr) + ); +} + #[test] fn test_bind_fail_leaves_socket_unbound() { let wasm = run_build_script(file!(), "bind-fail-stays-unbound").unwrap(); diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/build.sh b/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/build.sh new file mode 100755 index 000000000000..b3d2a483d5fd --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +$CC main.c -o main diff --git a/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/main.c b/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/main.c new file mode 100644 index 000000000000..dad49669bfb7 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/bound-tcp-writable/main.c @@ -0,0 +1,68 @@ +/* + * Verify that a successfully bound TCP socket is immediately reported writable + * by select(2), matching Linux semantics. + * + * On Linux: + * int fd = socket(...); bind(fd, ...); + * select(fd+1, NULL, &wfds, NULL, &zero_timeout); + * returns 1 — the socket is writable right away. + */ +#include +#include +#include +#include +#include +#include +#include + +int main(void) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + perror("socket"); + return 1; + } + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(0); + if (inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) != 1) { + fprintf(stderr, "inet_pton failed\n"); + close(fd); + return 1; + } + + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(fd); + return 1; + } + + /* Zero-timeout select: only report what is ready RIGHT NOW. */ + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + struct timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + int n = select(fd + 1, NULL, &wfds, NULL, &tv); + if (n < 0) { + perror("select"); + close(fd); + return 1; + } + + if (n == 0 || !FD_ISSET(fd, &wfds)) { + fprintf(stderr, + "bound TCP socket not reported writable by select " + "(n=%d) — expected writable immediately after bind\n", + n); + close(fd); + return 1; + } + + close(fd); + printf("bound TCP socket is writable\n"); + return 0; +} diff --git a/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/build.sh b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/build.sh new file mode 100755 index 000000000000..b3d2a483d5fd --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +set -euo pipefail +$CC main.c -o main diff --git a/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c new file mode 100644 index 000000000000..35f08bd8f311 --- /dev/null +++ b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c @@ -0,0 +1,157 @@ +/* + * Verify that a connected TCP socket keeps its local port reserved. + * + * POSIX / Linux behaviour: + * - A socket that has completed connect() holds its local (ephemeral) port + * for its entire lifetime. + * - Attempting to bind a *different* socket to the same local address while + * the first socket is still connected must fail with EADDRINUSE. + * - After the connected socket is closed the port is released and a new + * bind() to the same address must succeed. + * + * Steps + * 1. Create a server socket and listen on 127.0.0.1:0. + * 2. Bind a client socket to 127.0.0.1:0 (ephemeral) and connect to server. + * 3. Record the client's local address via getsockname. + * 4. Try to bind a third socket to that exact local address → EADDRINUSE. + * 5. Close the connected client socket. + * 6. Bind a third socket to the same address again → must succeed. + */ +#include +#include +#include +#include +#include +#include + +int main(void) { + /* ---- step 1: server ---- */ + int server = socket(AF_INET, SOCK_STREAM, 0); + if (server < 0) { + perror("socket(server)"); + return 1; + } + + int one = 1; + setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + struct sockaddr_in srv_addr; + memset(&srv_addr, 0, sizeof(srv_addr)); + srv_addr.sin_family = AF_INET; + srv_addr.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr); + + if (bind(server, (struct sockaddr*)&srv_addr, sizeof(srv_addr)) < 0) { + perror("bind(server)"); + close(server); + return 1; + } + if (listen(server, 1) < 0) { + perror("listen"); + close(server); + return 1; + } + + socklen_t srv_len = sizeof(srv_addr); + getsockname(server, (struct sockaddr*)&srv_addr, &srv_len); + + /* ---- step 2: client — bind to ephemeral port then connect ---- */ + int client = socket(AF_INET, SOCK_STREAM, 0); + if (client < 0) { + perror("socket(client)"); + close(server); + return 1; + } + + struct sockaddr_in cli_bind; + memset(&cli_bind, 0, sizeof(cli_bind)); + cli_bind.sin_family = AF_INET; + cli_bind.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &cli_bind.sin_addr); + + if (bind(client, (struct sockaddr*)&cli_bind, sizeof(cli_bind)) < 0) { + perror("bind(client)"); + close(server); + close(client); + return 1; + } + if (connect(client, (struct sockaddr*)&srv_addr, sizeof(srv_addr)) < 0) { + perror("connect"); + close(server); + close(client); + return 1; + } + + /* ---- step 3: record the client's local port ---- */ + struct sockaddr_in cli_local; + socklen_t cli_len = sizeof(cli_local); + memset(&cli_local, 0, sizeof(cli_local)); + if (getsockname(client, (struct sockaddr*)&cli_local, &cli_len) < 0) { + perror("getsockname(client)"); + close(server); + close(client); + return 1; + } + int cli_port = (int)ntohs(cli_local.sin_port); + if (cli_port == 0) { + fprintf(stderr, "client local port is 0 after connect\n"); + close(server); + close(client); + return 1; + } + + /* ---- step 4: rebind to same port while client is still connected ---- */ + int probe = socket(AF_INET, SOCK_STREAM, 0); + if (probe < 0) { + perror("socket(probe)"); + close(server); + close(client); + return 1; + } + + if (bind(probe, (struct sockaddr*)&cli_local, sizeof(cli_local)) == 0) { + fprintf(stderr, + "bind to port %d succeeded while client socket is still connected " + "(expected EADDRINUSE)\n", + cli_port); + close(probe); + close(server); + close(client); + return 1; + } + if (errno != EADDRINUSE) { + fprintf(stderr, + "bind to port %d failed with errno %d (%s), expected EADDRINUSE\n", + cli_port, errno, strerror(errno)); + close(probe); + close(server); + close(client); + return 1; + } + close(probe); + + /* ---- step 5: close the connected client ---- */ + close(client); + + /* ---- step 6: now the port must be available again ---- */ + int probe2 = socket(AF_INET, SOCK_STREAM, 0); + if (probe2 < 0) { + perror("socket(probe2)"); + close(server); + return 1; + } + + if (bind(probe2, (struct sockaddr*)&cli_local, sizeof(cli_local)) < 0) { + fprintf(stderr, + "bind to port %d failed after client socket was closed: %s\n", + cli_port, strerror(errno)); + close(probe2); + close(server); + return 1; + } + close(probe2); + close(server); + + printf("connected socket holds its local port\n"); + return 0; +} From 88c932bc1154d3a2a20c11d11cab8b0e144ecc19 Mon Sep 17 00:00:00 2001 From: Arshia Date: Thu, 30 Apr 2026 11:55:31 +0400 Subject: [PATCH 13/13] Address review comments Co-authored-by: Copilot --- lib/virtual-net/src/client.rs | 11 +++++ lib/wasix/tests/wasm_tests/socket_tests.rs | 16 +++++++ .../connect-holds-local-port/main.c | 42 +++++++++++++++---- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/virtual-net/src/client.rs b/lib/virtual-net/src/client.rs index 96922ddaf872..7b3826153534 100644 --- a/lib/virtual-net/src/client.rs +++ b/lib/virtual-net/src/client.rs @@ -936,6 +936,17 @@ impl RemoteSocket { .lock() .unwrap() .remove(&self.socket_id); + self.common + .accept_tx + .lock() + .unwrap() + .remove(&self.socket_id); + self.common.sent_tx.lock().unwrap().remove(&self.socket_id); + self.common.handlers.lock().unwrap().remove(&self.socket_id); + + if let Some((child_id, _)) = self.pending_accept.take() { + self.common.recv_tx.lock().unwrap().remove(&child_id); + } } async fn io_socket(&self, req: RequestType) -> ResponseType { diff --git a/lib/wasix/tests/wasm_tests/socket_tests.rs b/lib/wasix/tests/wasm_tests/socket_tests.rs index 500440a8f8bf..d7abf2b225d1 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests.rs +++ b/lib/wasix/tests/wasm_tests/socket_tests.rs @@ -62,6 +62,22 @@ fn test_bind_port_zero_keeps_same_port_across_connect() { ); } +#[test] +// https://github.com/wasmerio/wasmer/issues/6403 +fn test_connect_holds_local_port() { + let wasm = run_build_script(file!(), "connect-holds-local-port").unwrap(); + let result = run_wasm_with_result(&wasm, wasm.parent().unwrap()).unwrap(); + let stdout = String::from_utf8_lossy(&result.stdout); + assert_eq!( + stdout.trim(), + "connected socket holds its local port", + "exit_code={:?}\nstdout:\n{}\nstderr:\n{}", + result.exit_code, + stdout, + String::from_utf8_lossy(&result.stderr) + ); +} + #[test] fn test_bound_tcp_socket_is_writable() { let wasm = run_build_script(file!(), "bound-tcp-writable").unwrap(); diff --git a/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c index 35f08bd8f311..172b03a00f48 100644 --- a/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c +++ b/lib/wasix/tests/wasm_tests/socket_tests/connect-holds-local-port/main.c @@ -6,8 +6,9 @@ * for its entire lifetime. * - Attempting to bind a *different* socket to the same local address while * the first socket is still connected must fail with EADDRINUSE. - * - After the connected socket is closed the port is released and a new - * bind() to the same address must succeed. + * - After the connected socket is closed the explicit reservation is gone. + * A new bind() with SO_REUSEADDR should then succeed; without that Linux + * may still reject the bind because the connection can remain in TIME_WAIT. * * Steps * 1. Create a server socket and listen on 127.0.0.1:0. @@ -15,7 +16,8 @@ * 3. Record the client's local address via getsockname. * 4. Try to bind a third socket to that exact local address → EADDRINUSE. * 5. Close the connected client socket. - * 6. Bind a third socket to the same address again → must succeed. + * 6. Bind a third socket to the same address again with SO_REUSEADDR → must + * succeed. */ #include #include @@ -39,7 +41,16 @@ int main(void) { memset(&srv_addr, 0, sizeof(srv_addr)); srv_addr.sin_family = AF_INET; srv_addr.sin_port = htons(0); - inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr); + int inet_result = inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr); + if (inet_result != 1) { + if (inet_result == 0) { + fprintf(stderr, "inet_pton(server) could not parse 127.0.0.1\n"); + } else { + perror("inet_pton(server)"); + } + close(server); + return 1; + } if (bind(server, (struct sockaddr*)&srv_addr, sizeof(srv_addr)) < 0) { perror("bind(server)"); @@ -53,7 +64,11 @@ int main(void) { } socklen_t srv_len = sizeof(srv_addr); - getsockname(server, (struct sockaddr*)&srv_addr, &srv_len); + if (getsockname(server, (struct sockaddr*)&srv_addr, &srv_len) < 0) { + perror("getsockname(server)"); + close(server); + return 1; + } /* ---- step 2: client — bind to ephemeral port then connect ---- */ int client = socket(AF_INET, SOCK_STREAM, 0); @@ -67,7 +82,18 @@ int main(void) { memset(&cli_bind, 0, sizeof(cli_bind)); cli_bind.sin_family = AF_INET; cli_bind.sin_port = htons(0); - inet_pton(AF_INET, "127.0.0.1", &cli_bind.sin_addr); + inet_result = inet_pton(AF_INET, "127.0.0.1", &cli_bind.sin_addr); + if (inet_result != 1) { + if (inet_result == 0) { + fprintf(stderr, "inet_pton(client) could not parse 127.0.0.1\n"); + } else { + perror("inet_pton(client)"); + } + close(server); + close(client); + return 1; + } + setsockopt(client, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); if (bind(client, (struct sockaddr*)&cli_bind, sizeof(cli_bind)) < 0) { perror("bind(client)"); @@ -140,10 +166,12 @@ int main(void) { close(server); return 1; } + setsockopt(probe2, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); if (bind(probe2, (struct sockaddr*)&cli_local, sizeof(cli_local)) < 0) { fprintf(stderr, - "bind to port %d failed after client socket was closed: %s\n", + "bind to port %d failed after client socket was closed even with " + "SO_REUSEADDR: %s\n", cli_port, strerror(errno)); close(probe2); close(server);