From 7388cfe1f26afb896298e69120a455d62114a38c Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 14:35:17 -0700 Subject: [PATCH 1/6] Fix wasip3 blocking UDP subtask handle leaks On wasip3, blocking recv/send that wait for a pending subtask must drop the component-model handle when the wait completes. The nonblocking paths already did this; the blocking success paths only cleared the handle field and leaked. - recv: call wasip3_recv_end after a successful wait (drop + clear) - send: do not zero send_subtask before the shared drop block - Add dual-process blocking UDP roundtrip suite test (64 cycles) --- libc-bottom-half/sources/udp.c | 6 +- test/CMakeLists.txt | 5 ++ .../sockets-client-udp-blocking-roundtrips.c | 64 ++++++++++++++++++ .../sockets-server-udp-blocking-roundtrips.c | 66 +++++++++++++++++++ 4 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 test/src/sockets-client-udp-blocking-roundtrips.c create mode 100644 test/src/sockets-server-udp-blocking-roundtrips.c diff --git a/libc-bottom-half/sources/udp.c b/libc-bottom-half/sources/udp.c index 126310a6a..f8ae45764 100644 --- a/libc-bottom-half/sources/udp.c +++ b/libc-bottom-half/sources/udp.c @@ -799,8 +799,8 @@ static ssize_t udp_recvfrom(void *data, void *buffer, size_t length, int flags, return -1; } - streams->recv_subtask = 0; - streams->flags |= UDP_STREAMS_RECV_READY; + // Same completion path as nonblocking I/O: drop then clear. + wasip3_recv_end(streams, &event); } else { wasip3_event_t event; __wasilibc_poll_waitable(streams->recv_subtask, &event); @@ -933,7 +933,7 @@ static int wasip3_send_resolve(udp_socket_t *socket, bool should_block) { errno = EWOULDBLOCK; return -1; } - streams->send_subtask = 0; + // Leave send_subtask set so the shared drop below runs. } else { wasip3_event_t event; __wasilibc_poll_waitable(streams->send_subtask, &event); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 72d753cb8..a15098ce5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -737,6 +737,7 @@ if (NOT (WASI STREQUAL "p1")) add_sockets_test_executable(sockets-client-hangup-while-receiving.c) add_sockets_test_executable(sockets-client-hangup-while-sending.c) add_sockets_test_executable(sockets-client-udp-blocking.c) + add_sockets_test_executable(sockets-client-udp-blocking-roundtrips.c) add_sockets_test_executable(sockets-multiple-client.c) add_sockets_test_executable(sockets-server.c) add_sockets_test_executable(sockets-server-handle-hangups.c) @@ -745,6 +746,7 @@ if (NOT (WASI STREQUAL "p1")) add_sockets_test_executable(sockets-server-hangup-during-recv.c) add_sockets_test_executable(sockets-server-hangup-during-send.c) add_sockets_test_executable(sockets-server-udp-blocking.c) + add_sockets_test_executable(sockets-server-udp-blocking-roundtrips.c) add_sockets_test_executable(sockets-multiple-server.c) function(sockets_test test_name client server) @@ -768,6 +770,9 @@ if (NOT (WASI STREQUAL "p1")) sockets_test(sockets sockets-client.wasm sockets-server.wasm) sockets_test(sockets-udp-blocking sockets-client-udp-blocking.wasm sockets-server-udp-blocking.wasm) + sockets_test(sockets-udp-blocking-roundtrips + sockets-client-udp-blocking-roundtrips.wasm + sockets-server-udp-blocking-roundtrips.wasm) sockets_test(sockets-multiple sockets-multiple-client.wasm sockets-multiple-server.wasm NCLIENTS 10) diff --git a/test/src/sockets-client-udp-blocking-roundtrips.c b/test/src/sockets-client-udp-blocking-roundtrips.c new file mode 100644 index 000000000..19bc6f54d --- /dev/null +++ b/test/src/sockets-client-udp-blocking-roundtrips.c @@ -0,0 +1,64 @@ +#include "test.h" +#include +#include +#include +#include +#include +#include +#include + +#define TEST(c) \ + do { \ + errno = 0; \ + if (!(c)) \ + t_error("%s failed (errno = %d)\n", #c, errno); \ + } while (0) + +// Pair with sockets-server-udp-blocking-roundtrips.c. Many blocking send/recv +// cycles exercise wasip3 subtask completion on both directions. +#define NROUNDS 64 +#define BUFSIZE 256 + +void test_udp_client(int server_port) { + int socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + TEST(socket_fd != -1); + if (socket_fd == -1) + return; + + struct sockaddr_in sockaddr_in; + sockaddr_in.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sockaddr_in.sin_family = AF_INET; + sockaddr_in.sin_port = htons(server_port); + + TEST(connect(socket_fd, (struct sockaddr *)&sockaddr_in, + sizeof(sockaddr_in)) != -1); + + for (int i = 0; i < NROUNDS; i++) { + char message[64]; + int len = snprintf(message, sizeof(message), "roundtrip-%d", i); + TEST(len > 0 && len < (int)sizeof(message)); + + char client_buffer[BUFSIZE]; + TEST(send(socket_fd, message, len, 0) == len); + int bytes_received = recv(socket_fd, client_buffer, BUFSIZE, 0); + TEST(bytes_received == len); + if (bytes_received == len) + TEST(memcmp(message, client_buffer, len) == 0); + } + + close(socket_fd); +} + +int main(int argc, char *argv[]) { + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + int port; + if (sscanf(argv[1], "%d", &port) != 1) { + fprintf(stderr, "Invalid port number: %s\n", argv[1]); + return 1; + } + test_udp_client(port); + return t_status; +} diff --git a/test/src/sockets-server-udp-blocking-roundtrips.c b/test/src/sockets-server-udp-blocking-roundtrips.c new file mode 100644 index 000000000..f9d210e8d --- /dev/null +++ b/test/src/sockets-server-udp-blocking-roundtrips.c @@ -0,0 +1,66 @@ +#include "test.h" +#include +#include +#include +#include +#include +#include +#include + +#define TEST(c) \ + do { \ + errno = 0; \ + if (!(c)) \ + t_error("%s failed (errno = %d)\n", #c, errno); \ + } while (0) + +// Many blocking UDP recv/send cycles. On wasip3, each blocking wait that +// completes a pending subtask must drop that subtask handle. Clearing the +// handle without drop leaks component-model resources across iterations. +// The server blocks in recvfrom before each client send so the receive +// subtask is often still outstanding when the wait completes. +#define NROUNDS 64 +#define BUFSIZE 256 + +void run_udp_server(void) { + int server_socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + TEST(server_socket_fd != -1); + if (server_socket_fd == -1) + return; + + struct sockaddr_in server_address; + socklen_t server_address_len = sizeof(server_address); + server_address.sin_family = AF_INET; + server_address.sin_addr.s_addr = htonl(INADDR_ANY); + server_address.sin_port = 0; + TEST(bind(server_socket_fd, (struct sockaddr *)&server_address, + sizeof(server_address)) != -1); + if (t_status != 0) + return; + + TEST(getsockname(server_socket_fd, (struct sockaddr *)&server_address, + &server_address_len) != -1); + + printf("%d\n", ntohs(server_address.sin_port)); + fflush(stdout); + + for (int i = 0; i < NROUNDS; i++) { + struct sockaddr_in client_address; + socklen_t address_len = sizeof(client_address); + char buffer[BUFSIZE]; + int bytes_read = recvfrom(server_socket_fd, buffer, BUFSIZE, 0, + (struct sockaddr *)&client_address, &address_len); + TEST(bytes_read > 0); + if (bytes_read <= 0) + break; + TEST(sendto(server_socket_fd, buffer, bytes_read, 0, + (struct sockaddr *)&client_address, address_len) == bytes_read); + } + + close(server_socket_fd); +} + +int main(void) { + run_udp_server(); + return t_status; +} From 08412cf798a5b118433e3af7b4c074327e31a763 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 14:47:11 -0700 Subject: [PATCH 2/6] ci: retrigger after GitHub release download 503s From dc9e40d8f40a6f301e7ac8eefc752d6ac67076f6 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 14:54:29 -0700 Subject: [PATCH 3/6] ci: retrigger after remaining download and timeout flakes From 106ef6ee14eba09c9dddd86429199d4fae288965 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 15:19:48 -0700 Subject: [PATCH 4/6] ci: retrigger after wasi-sdk 503 on Python wasip3 coop-threads From 36af83fae89adfa21ed1da234d15ff9cfed1f5aa Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 15:37:19 -0700 Subject: [PATCH 5/6] ci: retrigger after unrelated shared_poll-connect flake on Windows From c62b6304a30929b7a961f74382a18a29c88a33b1 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Wed, 12 Aug 2026 19:42:04 -0700 Subject: [PATCH 6/6] test: red-green wasip3 UDP subtask leaks via max-resources I/O-only roundtrips pass with or without subtask drop because the payload is already in guest storage after wait. Cap wasmtime concurrent resources (-Smax-resources=128) on the dual-process suite so undropped HostTask entries fail with "resource table has no free keys" while a correct drop stays under the cap. Raise rounds to 200 and timeout to 15s. --- test/CMakeLists.txt | 37 +++++++++++++++---- test/socket-test.cmake | 28 ++++++++++++-- .../sockets-client-udp-blocking-roundtrips.c | 6 ++- .../sockets-server-udp-blocking-roundtrips.c | 10 +++-- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a15098ce5..8fe517798 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -751,18 +751,28 @@ if (NOT (WASI STREQUAL "p1")) function(sockets_test test_name client server) set(options) - set(oneValueArgs NCLIENTS) + set(oneValueArgs NCLIENTS MAX_RESOURCES TIMEOUT_S) set(multiValueArgs) cmake_parse_arguments(PARSE_ARGV 1 arg "${options}" "${oneValueArgs}" "${multiValueArgs}") + set(socket_test_args + -DENGINE=${ENGINE} + -DSERVER=$ + -DCLIENT=$ + -DNCLIENTS=${arg_NCLIENTS} + ) + if (arg_MAX_RESOURCES) + list(APPEND socket_test_args -DMAX_RESOURCES=${arg_MAX_RESOURCES}) + endif() + if (arg_TIMEOUT_S) + list(APPEND socket_test_args -DTIMEOUT_S=${arg_TIMEOUT_S}) + endif() + add_test( NAME "${test_name}" COMMAND ${CMAKE_COMMAND} - -DENGINE=${ENGINE} - -DSERVER=$ - -DCLIENT=$ - -DNCLIENTS=${arg_NCLIENTS} + ${socket_test_args} -P ${CMAKE_CURRENT_SOURCE_DIR}/socket-test.cmake ) endfunction() @@ -770,9 +780,20 @@ if (NOT (WASI STREQUAL "p1")) sockets_test(sockets sockets-client.wasm sockets-server.wasm) sockets_test(sockets-udp-blocking sockets-client-udp-blocking.wasm sockets-server-udp-blocking.wasm) - sockets_test(sockets-udp-blocking-roundtrips - sockets-client-udp-blocking-roundtrips.wasm - sockets-server-udp-blocking-roundtrips.wasm) + # wasip3: cap concurrent resources so a missing subtask drop fails the suite + # (wasmtime: "resource table has no free keys") while proper drop stays green. + # wasip2 has no async subtasks for this path; leave unlimited. + if (WASI STREQUAL "p3") + sockets_test(sockets-udp-blocking-roundtrips + sockets-client-udp-blocking-roundtrips.wasm + sockets-server-udp-blocking-roundtrips.wasm + MAX_RESOURCES 128 + TIMEOUT_S 15) + else() + sockets_test(sockets-udp-blocking-roundtrips + sockets-client-udp-blocking-roundtrips.wasm + sockets-server-udp-blocking-roundtrips.wasm) + endif() sockets_test(sockets-multiple sockets-multiple-client.wasm sockets-multiple-server.wasm NCLIENTS 10) diff --git a/test/socket-test.cmake b/test/socket-test.cmake index 37a76d62c..bdee80a89 100644 --- a/test/socket-test.cmake +++ b/test/socket-test.cmake @@ -9,6 +9,28 @@ if (NOT NCLIENTS) set(NCLIENTS 1) endif() +# Base engine flags for wasip3 dual-process socket tests. +set(ENGINE_FLAGS + -Wcomponent-model-async + -Wcomponent-model-threading + -Sp3,inherit-network +) + +# Optional cap on wasmtime resource tables. When set, undropped wasip3 subtask +# HostTask entries accumulate in the concurrent ResourceTable and eventually +# trap with "resource table has no free keys". Used to make blocking UDP +# subtask-drop regressions fail before the fix and pass after it. +if (MAX_RESOURCES) + list(APPEND ENGINE_FLAGS -Smax-resources=${MAX_RESOURCES}) +endif() + +# Serialize flags for embedding inside bash -c strings below. +string(JOIN " " ENGINE_FLAGS_STR ${ENGINE_FLAGS}) + +if (NOT TIMEOUT_S) + set(TIMEOUT_S 5) +endif() + foreach(i RANGE 1 ${NCLIENTS}) # This is a bit tricky to setup, but the flow is: # @@ -36,15 +58,15 @@ read port # read the first line of stdin from the previous process echo $port # forward this line to the next process, or out to cmake itself exec 2>&1 # close our stdout and replace it with stderr cat <&0 & # forward the rest of stdin to stderr so it shows up in cmake -exec ${ENGINE} -Wcomponent-model-async -Wcomponent-model-threading -Sp3,inherit-network ${CLIENT} \"$port\" +exec ${ENGINE} ${ENGINE_FLAGS_STR} ${CLIENT} \"$port\" ") list(APPEND CLIENTS COMMAND bash -c ${client_script}) endforeach() execute_process( - COMMAND ${ENGINE} -Wcomponent-model-async -Wcomponent-model-threading -Sp3,inherit-network ${SERVER} + COMMAND ${ENGINE} ${ENGINE_FLAGS} ${SERVER} ${CLIENTS} - TIMEOUT 5 + TIMEOUT ${TIMEOUT_S} COMMAND_ERROR_IS_FATAL ANY COMMAND_ECHO STDOUT ) diff --git a/test/src/sockets-client-udp-blocking-roundtrips.c b/test/src/sockets-client-udp-blocking-roundtrips.c index 19bc6f54d..4d0a1f339 100644 --- a/test/src/sockets-client-udp-blocking-roundtrips.c +++ b/test/src/sockets-client-udp-blocking-roundtrips.c @@ -15,8 +15,10 @@ } while (0) // Pair with sockets-server-udp-blocking-roundtrips.c. Many blocking send/recv -// cycles exercise wasip3 subtask completion on both directions. -#define NROUNDS 64 +// cycles exercise wasip3 subtask completion on both directions. On wasip3 CI +// this runs under -Smax-resources=128 so missing subtask drops fail the +// resource table while correct drops stay green (see server file comment). +#define NROUNDS 200 #define BUFSIZE 256 void test_udp_client(int server_port) { diff --git a/test/src/sockets-server-udp-blocking-roundtrips.c b/test/src/sockets-server-udp-blocking-roundtrips.c index f9d210e8d..d924d1ca0 100644 --- a/test/src/sockets-server-udp-blocking-roundtrips.c +++ b/test/src/sockets-server-udp-blocking-roundtrips.c @@ -16,10 +16,12 @@ // Many blocking UDP recv/send cycles. On wasip3, each blocking wait that // completes a pending subtask must drop that subtask handle. Clearing the -// handle without drop leaks component-model resources across iterations. -// The server blocks in recvfrom before each client send so the receive -// subtask is often still outstanding when the wait completes. -#define NROUNDS 64 +// handle without drop leaves HostTask entries in wasmtime's concurrent +// resource table. CI runs this pair with -Smax-resources=128 so a leak +// fails with "resource table has no free keys" while a correct drop stays +// under the cap. The server blocks in recvfrom before each client send so +// the receive subtask is often still outstanding when the wait completes. +#define NROUNDS 200 #define BUFSIZE 256 void run_udp_server(void) {