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..8fe517798 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,22 +746,33 @@ 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) 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() @@ -768,6 +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) + # 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 new file mode 100644 index 000000000..4d0a1f339 --- /dev/null +++ b/test/src/sockets-client-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) + +// Pair with sockets-server-udp-blocking-roundtrips.c. Many blocking send/recv +// 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) { + 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..d924d1ca0 --- /dev/null +++ b/test/src/sockets-server-udp-blocking-roundtrips.c @@ -0,0 +1,68 @@ +#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 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) { + 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; +}