Stream: git-wasmtime

Topic: wasmtime / issue #12589 [wasi-sockets] reconnecting UDP s...


view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 20:55):

dicej edited issue #12589:

I've been adding WASIp2 support to mio and triaging failures in its test suite. This test is failing, which I tracked down to wasmtime-wasi spuriously changing the local address of a UDP socket when wasi:sockets/udp#udp-socket.stream is called a second time on the same socket (equivalent to calling connect(2) a second time in POSIX). The test fails because it uses the old local address, not realizing the socket suddenly has a new local address.

Here's a minimal test case:

// udp.rs
use std::{error::Error, net::UdpSocket};

fn main() -> Result<(), Box<dyn Error>> {
    let socket1 = UdpSocket::bind("127.0.0.1:0")?;
    let socket2 = UdpSocket::bind("127.0.0.1:0")?;

    let address1 = socket1.local_addr()?;
    let address2 = socket2.local_addr()?;

    println!(
        "before:\n  \
         socket1 local address: {address1:?}\n  \
         socket2 local address: {address2:?}"
    );

    socket1.connect(address1)?;
    socket1.connect(address2)?;
    socket2.connect(address1)?;

    println!(
        "after:\n  \
         socket1 local address: {address1:?} remote address: {remote1:?}\n  \
         socket2 local address: {address2:?} remote address: {remote2:?}",
        address1 = socket1.local_addr()?,
        address2 = socket2.local_addr()?,
        remote1 = socket1.peer_addr()?,
        remote2 = socket2.peer_addr()?
    );

    let data = b"foobar";
    socket1.send(data)?;

    let mut buffer = [0u8; 20];
    let count = socket2.recv(&mut buffer)?;

    assert_eq!(data, &buffer[..count]);

    println!("success!");

    Ok(())
}

If you build and run that natively on e.g. Linux using rustc udp.rs && ./udp, you'll see it succeed, with socket1's local address remaining unchanged before and after the UdpSocket::connect calls.

If you build and run for WASIp2 using rustc --target=wasm32-wasip2 udp.rs && wasmtime run -Sudp,inherit-network udp.wasm, you'll see it hang indefinitely due to socket1's local address changing due to the second UdpSocket::connect call.

While digging into this, I found that wasmtime-wasi proactively disconnects the socket by calling rustix::net::connect_unspec prior to reconnecting. When I comment out that code, the issue is fixed:

diff --git a/crates/wasi/src/p2/host/udp.rs b/crates/wasi/src/p2/host/udp.rs
index 1ef8350399..5dd426e736 100644
--- a/crates/wasi/src/p2/host/udp.rs
+++ b/crates/wasi/src/p2/host/udp.rs
@@ -71,9 +71,9 @@ impl udp::HostUdpSocket for WasiSocketsCtxView<'_> {
         //   if there isn't a disconnect in between.

         // Step #1: Disconnect
-        if socket.is_connected() {
-            socket.disconnect()?;
-        }
+        // if socket.is_connected() {
+        //     socket.disconnect()?;
+        // }

         // Step #2: (Re)connect
         if let Some(connect_addr) = remote_address {
diff --git a/crates/wasi/src/sockets/udp.rs b/crates/wasi/src/sockets/udp.rs
index 482c6aa090..ba1e93f18d 100644
--- a/crates/wasi/src/sockets/udp.rs
+++ b/crates/wasi/src/sockets/udp.rs
@@ -162,10 +162,10 @@ impl UdpSocket {
         //   if there isn't a disconnect in between.

         // Step #1: Disconnect
-        if let UdpState::Connected(..) = self.udp_state {
-            udp_disconnect(&self.socket)?;
-            self.udp_state = UdpState::Bound;
-        }
+        // if let UdpState::Connected(..) = self.udp_state {
+        //     udp_disconnect(&self.socket)?;
+        //     self.udp_state = UdpState::Bound;
+        // }
         // Step #2: (Re)connect
         connect(&self.socket, &addr).map_err(|error| match error {
             Errno::AFNOSUPPORT => ErrorCode::InvalidArgument, // See `udp_bind` implementation.

However, per the surrounding comments, that code is there for a reason, so I assume that removing it isn't the right solution. @badeend this seems to be related to https://github.com/bytecodealliance/wasmtime/pull/7411, so perhaps you have thoughts?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 20:56):

dicej added the bug label to Issue #12589.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 21:55):

badeend commented on issue #12589:

Every call to udp-socket::connect is allowed to change the local socket address. This is documented on the method itself: https://github.com/WebAssembly/WASI/blob/41d0de898ec7568e032129156a3bc567d7ebea5d/proposals/sockets/wit-0.3.0-draft/types.wit#L547-L550

AFAIK, this is standard POSIX behavior. Are you sure you're testing exactly the same code as the mio test? Because the mio test you linked reads: (no local address rebinding)

    socket1.connect(address2).unwrap();
    socket2.connect(address1).unwrap();
    socket3.connect(address1).unwrap();

whereas the code snippet you posted rebinds socket1:

    socket1.connect(address1)?;
    socket1.connect(address2)?;
    socket2.connect(address1)?;

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 22:27):

dicej commented on issue #12589:

Are you sure you're testing exactly the same code as the mio test?

No, it's not exactly the same; I reduced the mio test to a minimal reproduction. Note that the mio test _does_ call connect twice on socket1: scroll down to https://github.com/tokio-rs/mio/blob/66ac9fab79bf191218488c4f35c99d13935b7e12/tests/udp_socket.rs#L442

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 22:32):

dicej commented on issue #12589:

BTW, if POSIX allows connect to change the local address, then the mio test is just wrong, and I can submit a PR to fix it (e.g. by calling socket3.connect(socket1.local_addr().unwrap()).unwrap() after the socket1.connect(address3).unwrap() line). It would help to have a document to reference to justify that, though, since apparently WASI is the only platform where the test currently fails.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 22:35):

dicej commented on issue #12589:

(e.g. by calling socket3.connect(socket1.local_addr().unwrap()).unwrap() after the socket1.connect(address3).unwrap() line)

Oof, actually not sure that would work, because then socket3's local address could change. So I guess I'm not even sure how I would fix it, since every call to connect would invalidate the local address previously used.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 23:10):

alexcrichton commented on issue #12589:

@badeend do you have further links for where the clause you linked in the WASI docs come from? Naively to me that seems pretty surprising that connect can arbitrarily change the locally bound address, so I'd be curious to read up more there. Is this something, for example, where platforms disagree on behavior and WASI is specifying a sort of least-common-denominator semantics?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 13 2026 at 23:28):

dicej edited a comment on issue #12589:

(e.g. by calling socket3.connect(socket1.local_addr().unwrap()).unwrap() after the socket1.connect(address3).unwrap() line)

Oof, actually not sure that would work, because then socket3's local address could change. So I guess I'm not even sure how I would fix it, since every call to connect could invalidate the local address previously used.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 14 2026 at 16:07):

badeend commented on issue #12589:

to me that seems pretty surprising that connect can arbitrarily change the locally bound address

Yes, it is surprising, and Yes it does happen :P But only if the socket wasn't bound to a specific address/port.

Let's say, the first connect is to 127.0.0.1. The socket's local address will become some address on the loopback interface. Most likely, also 127.0.0.1.

Then, the same socket is reused to connect to 123.234.12.34. The loopback interface has no network path to that remote, so the local binding has to change in order to do something useful. On my machine (example below) that is 192.168.178.13 (windows), 172.22.83.10 (linux)


I was not able to find any authoritative documentation describing this in detail, or: at all. The current WASI spec is purely based on observed behavior.

To make sure I'm not going mad, I just ran this test twice on Windows and Linux; once with wasmtime as-is (_with_ the explicit disconnect), and once with the disconnect commented out:

Windows _with_ disconnect
#1. connect(    127.0.0.1:4321) changed local address to:      127.0.0.1:64916
#2. connect(    127.0.0.1:4322) changed local address to:      127.0.0.1:64916
#3. connect(123.234.12.34:4323) changed local address to: 192.168.178.13:64916
Windows _without_ disconnect
#1. connect(    127.0.0.1:4321) changed local address to:      127.0.0.1:61198
#2. connect(    127.0.0.1:4322) changed local address to:      127.0.0.1:61198
#3. connect(123.234.12.34:4323) changed local address to: 192.168.178.13:61198
Linux _with_ disconnect
#1. connect(    127.0.0.1:4321) changed local address to:      127.0.0.1:37851
#2. connect(    127.0.0.1:4322) changed local address to:      127.0.0.1:44532
#3. connect(123.234.12.34:4323) changed local address to:   172.22.83.10:49662
Linux _without_ disconnect
#1. connect(    127.0.0.1:4321) changed local address to:      127.0.0.1:32810
#2. connect(    127.0.0.1:4322) changed local address to:      127.0.0.1:32810
#3. <<connect fails with EINVAL>>

Notes:


if POSIX allows connect to change the local address, then the mio test is just wrong,

Their test consistently passes on major platforms, so it is not obviously incorrect. However, it does rely on behavior that is not strictly guaranteed. With the specific parameters they use, that behavior just happens to be stable across current platforms.

My takeaways:

#1. connect(    127.0.0.1:4321) changed local address to:    127.0.0.1:37494
#2. connect(    127.0.0.1:4322) changed local address to:    127.0.0.1:37494
#3. connect(123.234.12.34:4323) changed local address to: 172.22.83.10:33292

This likely restores the behavior expected by the mio test while still handling the Linux EINVAL case when necessary.

I've opened https://github.com/bytecodealliance/wasmtime/pull/12595 as a potential fix.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 14 2026 at 20:43):

alexcrichton commented on issue #12589:

Oh wow that's fascinating! Thanks so much for taking the time to test and investigate this, and I agree that #12595 is the way to go here. Definitely learn something new every day :)

view this post on Zulip Wasmtime GitHub notifications bot (Feb 16 2026 at 01:37):

dicej closed issue #12589:

I've been adding WASIp2 support to mio and triaging failures in its test suite. This test is failing, which I tracked down to wasmtime-wasi spuriously changing the local address of a UDP socket when wasi:sockets/udp#udp-socket.stream is called a second time on the same socket (equivalent to calling connect(2) a second time in POSIX). The test fails because it uses the old local address, not realizing the socket suddenly has a new local address.

Here's a minimal test case:

// udp.rs
use std::{error::Error, net::UdpSocket};

fn main() -> Result<(), Box<dyn Error>> {
    let socket1 = UdpSocket::bind("127.0.0.1:0")?;
    let socket2 = UdpSocket::bind("127.0.0.1:0")?;

    let address1 = socket1.local_addr()?;
    let address2 = socket2.local_addr()?;

    println!(
        "before:\n  \
         socket1 local address: {address1:?}\n  \
         socket2 local address: {address2:?}"
    );

    socket1.connect(address1)?;
    socket1.connect(address2)?;
    socket2.connect(address1)?;

    println!(
        "after:\n  \
         socket1 local address: {address1:?} remote address: {remote1:?}\n  \
         socket2 local address: {address2:?} remote address: {remote2:?}",
        address1 = socket1.local_addr()?,
        address2 = socket2.local_addr()?,
        remote1 = socket1.peer_addr()?,
        remote2 = socket2.peer_addr()?
    );

    let data = b"foobar";
    socket1.send(data)?;

    let mut buffer = [0u8; 20];
    let count = socket2.recv(&mut buffer)?;

    assert_eq!(data, &buffer[..count]);

    println!("success!");

    Ok(())
}

If you build and run that natively on e.g. Linux using rustc udp.rs && ./udp, you'll see it succeed, with socket1's local address remaining unchanged before and after the UdpSocket::connect calls.

If you build and run for WASIp2 using rustc --target=wasm32-wasip2 udp.rs && wasmtime run -Sudp,inherit-network udp.wasm, you'll see it hang indefinitely due to socket1's local address changing due to the second UdpSocket::connect call.

While digging into this, I found that wasmtime-wasi proactively disconnects the socket by calling rustix::net::connect_unspec prior to reconnecting. When I comment out that code, the issue is fixed:

diff --git a/crates/wasi/src/p2/host/udp.rs b/crates/wasi/src/p2/host/udp.rs
index 1ef8350399..5dd426e736 100644
--- a/crates/wasi/src/p2/host/udp.rs
+++ b/crates/wasi/src/p2/host/udp.rs
@@ -71,9 +71,9 @@ impl udp::HostUdpSocket for WasiSocketsCtxView<'_> {
         //   if there isn't a disconnect in between.

         // Step #1: Disconnect
-        if socket.is_connected() {
-            socket.disconnect()?;
-        }
+        // if socket.is_connected() {
+        //     socket.disconnect()?;
+        // }

         // Step #2: (Re)connect
         if let Some(connect_addr) = remote_address {
diff --git a/crates/wasi/src/sockets/udp.rs b/crates/wasi/src/sockets/udp.rs
index 482c6aa090..ba1e93f18d 100644
--- a/crates/wasi/src/sockets/udp.rs
+++ b/crates/wasi/src/sockets/udp.rs
@@ -162,10 +162,10 @@ impl UdpSocket {
         //   if there isn't a disconnect in between.

         // Step #1: Disconnect
-        if let UdpState::Connected(..) = self.udp_state {
-            udp_disconnect(&self.socket)?;
-            self.udp_state = UdpState::Bound;
-        }
+        // if let UdpState::Connected(..) = self.udp_state {
+        //     udp_disconnect(&self.socket)?;
+        //     self.udp_state = UdpState::Bound;
+        // }
         // Step #2: (Re)connect
         connect(&self.socket, &addr).map_err(|error| match error {
             Errno::AFNOSUPPORT => ErrorCode::InvalidArgument, // See `udp_bind` implementation.

However, per the surrounding comments, that code is there for a reason, so I assume that removing it isn't the right solution. @badeend this seems to be related to https://github.com/bytecodealliance/wasmtime/pull/7411, so perhaps you have thoughts?

view this post on Zulip Wasmtime GitHub notifications bot (Feb 16 2026 at 18:40):

badeend commented on issue #12589:

From recollection, MacOS behaves the same as Windows. But I don't have a Mac machine to verify.

For posterity; the PR passed CI without special considerations for MacOS. This implies that it indeed behaves the same as Windows.

view this post on Zulip Wasmtime GitHub notifications bot (Feb 16 2026 at 20:13):

badeend commented on issue #12589:

So... fun fact. The same thing happens on TCP sockets as well. :monocle_face:

let client = TcpSocket::create(family).unwrap();
client.bind(IpSocketAddress::new(IpAddress::new_unspecified(family), 0)).unwrap();
println!("local address: {:?}", client.get_local_address().unwrap());
client.connect(listener_address).await.unwrap();
println!("local address: {:?}", client.get_local_address().unwrap());

prints:

local address:   0.0.0.0:36431
local address: 127.0.0.1:36431

I've opened https://github.com/bytecodealliance/wasmtime/pull/12601 to test this


Last updated: Feb 24 2026 at 04:36 UTC