From e2e00a2e507c6f9d68e165b89a980cb0602e116b Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 14:05:00 -0700 Subject: [PATCH 1/6] Fix wasip1 accept(NULL) crash and emulated mmap leak on pread failure wasip1 accept/accept4 always wrote the peer sockaddr after a successful sock_accept. POSIX allows both addr and addrlen to be NULL when the caller does not need the peer address; the write path null-dereferenced in that case. Guard the fill the same way wasip2/p3 do via __wasilibc_sockaddr_validate (both-null is allowed). Emulated mmap allocates a header+body block, then on non-MAP_ANON paths fills it with pread. On pread error it returned MAP_FAILED without freeing that allocation. Free the block and preserve errno. Host red-green: null memset crashes without the accept guard; free+errno path drops the allocation and keeps EIO. Signed-off-by: Sebastien Tardif --- libc-bottom-half/mman/mman.c | 4 ++++ libc-bottom-half/sources/accept-wasip1.c | 27 +++++++++++++++--------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/libc-bottom-half/mman/mman.c b/libc-bottom-half/mman/mman.c index 08e1acc1f..75cb8689b 100644 --- a/libc-bottom-half/mman/mman.c +++ b/libc-bottom-half/mman/mman.c @@ -90,6 +90,10 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd, if (nread < 0) { if (errno == EINTR) continue; + // Free the header allocation; pread already set errno. + int saved_errno = errno; + free(map); + errno = saved_errno; return MAP_FAILED; } if (nread == 0) diff --git a/libc-bottom-half/sources/accept-wasip1.c b/libc-bottom-half/sources/accept-wasip1.c index 94ee1a9e8..42514c823 100644 --- a/libc-bottom-half/sources/accept-wasip1.c +++ b/libc-bottom-half/sources/accept-wasip1.c @@ -18,11 +18,16 @@ int accept(int socket, struct sockaddr *restrict addr, return -1; } - // Clear sockaddr to indicate undefined address - memset(addr, 0, *addrlen); - // might be AF_UNIX or AF_INET - addr->sa_family = AF_UNSPEC; - *addrlen = sizeof(struct sockaddr); + // POSIX allows both addr and addrlen to be NULL when the peer address is + // unused. wasip2/p3 accept paths use __wasilibc_sockaddr_validate for the + // same both-null case. + if (addr != NULL && addrlen != NULL) { + // Clear sockaddr to indicate undefined address + memset(addr, 0, *addrlen); + // might be AF_UNIX or AF_INET + addr->sa_family = AF_UNSPEC; + *addrlen = sizeof(struct sockaddr); + } return ret; } @@ -44,11 +49,13 @@ int accept4(int socket, struct sockaddr *restrict addr, return -1; } - // Clear sockaddr to indicate undefined address - memset(addr, 0, *addrlen); - // might be AF_UNIX or AF_INET - addr->sa_family = AF_UNSPEC; - *addrlen = sizeof(struct sockaddr); + if (addr != NULL && addrlen != NULL) { + // Clear sockaddr to indicate undefined address + memset(addr, 0, *addrlen); + // might be AF_UNIX or AF_INET + addr->sa_family = AF_UNSPEC; + *addrlen = sizeof(struct sockaddr); + } return ret; } From 60590554711e594bb15e0fd138840af0c5ad33b4 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 15:43:40 -0700 Subject: [PATCH 2/6] test: cover accept(NULL) and emulated mmap pread failure Add suite tests for the #866 fixes: - sockets-accept-null-addr: accept/accept4 with NULL peer address (and a non-null control path). Registered for wasip2/p3 NETWORK suite and for wasip1 (non-V8) so accept-wasip1.c is exercised. - mmap-pread-fail: file-backed emulated mmap with O_WRONLY pread failure returns MAP_FAILED, survives leak-stress iterations, and still allows MAP_ANON afterward. Host red-green for both control-flow classes remains a local check; these tests make the regressions visible in the wasi-libc CI matrix. Signed-off-by: Sebastien Tardif --- test/CMakeLists.txt | 14 ++++ test/src/mmap-pread-fail.c | 54 +++++++++++++++ test/src/sockets-accept-null-addr.c | 101 ++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 test/src/mmap-pread-fail.c create mode 100644 test/src/sockets-accept-null-addr.c diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2634f2598..a0c719537 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -442,6 +442,11 @@ add_wasilibc_test(clock_nanosleep.c) add_wasilibc_test(chdir.c FS) add_wasilibc_test(close.c FS) add_wasilibc_test(pread.c FS) +# Emulated mmap free-on-pread-fail (#866). Links the optional mman library. +add_wasilibc_test(mmap-pread-fail.c FS + CFLAGS -D_WASI_EMULATED_MMAN + LDFLAGS -lwasi-emulated-mman + SHARED_LIBS "${SYSROOT_LIB}/libwasi-emulated-mman.so") add_wasilibc_test(external_env.c ENV VAR1=foo VAR2=bar) add_wasilibc_test(fadvise.c FS) # I'm not really entirely sure why this test is failing, it seemingly exits with @@ -689,6 +694,13 @@ add_external_entrypoint_test(external_entrypoint_pthread_tls.c) # ========= sockets-related tests =============================== +# wasip1 skips the bulk NETWORK suite, but #866 fixed a wasip1-only +# accept(NULL) crash. Register the focused case outside the p2/p3 block so +# wasip1 CI also exercises accept-wasip1.c when the engine supports sockets. +if (WASI STREQUAL "p1" AND NOT TEST_WITH_V8) + add_wasilibc_test(sockets-accept-null-addr.c NETWORK) +endif() + if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(poll-connect.c NETWORK) add_wasilibc_test(poll-badfd.c) @@ -712,6 +724,8 @@ if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(sockets-nonblocking-accept-multiple.c NETWORK) add_wasilibc_test(sockets-nonblocking-shutdown.c NETWORK) add_wasilibc_test(sockets-select-connecting-rw.c NETWORK) + # POSIX accept/accept4 with NULL peer address (#866 wasip1 crash class). + add_wasilibc_test(sockets-accept-null-addr.c NETWORK) add_wasilibc_test(sockets-listen-backlog.c NETWORK) add_wasilibc_test(sockets-sendto-connected.c NETWORK) add_wasilibc_test(sockets-recvfrom-connected.c NETWORK) diff --git a/test/src/mmap-pread-fail.c b/test/src/mmap-pread-fail.c new file mode 100644 index 000000000..72f9b9fb9 --- /dev/null +++ b/test/src/mmap-pread-fail.c @@ -0,0 +1,54 @@ +#include "test.h" +#include +#include +#include +#include +#include + +#define TEST(c) \ + do { \ + errno = 0; \ + if (!(c)) \ + t_error("%s failed (errno = %d)\n", #c, errno); \ + } while (0) + +// Emulated mmap allocates a header+body block, then fills it with pread for +// file-backed maps. On pread failure it must free that allocation, return +// MAP_FAILED, and leave errno set. Without free, repeated failures grow heap +// until mmap(MAP_ANON) fails with ENOMEM. +int main(void) { + char tmp[] = "testsuite-mmap-pread-fail"; + int wr_fd; + TEST((wr_fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600)) > 2); + TEST(write(wr_fd, "x", 1) == 1); + + // Single failing file-backed mmap: pread on O_WRONLY must fail. + errno = 0; + void *p = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, wr_fd, 0); + int mmap_errno = errno; + TEST(p == MAP_FAILED); + TEST(mmap_errno != 0); + + // Leak stress: many failing mmaps of 1 MiB. With free, this stays small; + // without free it exhausts wasm linear memory. + const size_t map_len = 1u << 20; + for (int i = 0; i < 256; i++) { + errno = 0; + p = mmap(NULL, map_len, PROT_READ, MAP_PRIVATE, wr_fd, 0); + mmap_errno = errno; + TEST(p == MAP_FAILED); + TEST(mmap_errno != 0); + } + + // After the failing path, MAP_ANON still works (memory was not leaked away). + errno = 0; + void *anon = + mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); + TEST(anon != MAP_FAILED); + memset(anon, 0xab, 4096); + TEST(munmap(anon, 4096) == 0); + + TEST(close(wr_fd) == 0); + TEST(unlink(tmp) == 0); + return t_status; +} diff --git a/test/src/sockets-accept-null-addr.c b/test/src/sockets-accept-null-addr.c new file mode 100644 index 000000000..9235a29eb --- /dev/null +++ b/test/src/sockets-accept-null-addr.c @@ -0,0 +1,101 @@ +#include "test.h" +#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) + +// POSIX allows accept/accept4 peer address args to both be NULL when the +// caller does not need the peer address. wasip1 previously null-dereferenced +// on that shape after a successful sock_accept. +static int listen_loopback(void) { + int lfd = socket(AF_INET, SOCK_STREAM, 0); + TEST(lfd >= 0); + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + TEST(bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) == 0); + TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); + TEST(listen(lfd, 1) == 0); + return lfd; +} + +static int connect_nonblock(const struct sockaddr_in *addr) { + int cfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); + TEST(cfd >= 0); + errno = 0; + int rc = connect(cfd, (const struct sockaddr *)addr, sizeof(*addr)); + if (rc != 0 && errno != EINPROGRESS) + t_error("connect failed (rc = %d, errno = %d)\n", rc, errno); + return cfd; +} + +static void check_ping(int cfd, int sfd) { + TEST(send(cfd, "ping", 4, 0) == 4); + char buf[4] = {0}; + TEST(recv(sfd, buf, sizeof(buf), 0) == 4); + TEST(memcmp(buf, "ping", 4) == 0); +} + +int main(void) { + // accept(fd, NULL, NULL) + { + int lfd = listen_loopback(); + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); + int cfd = connect_nonblock(&addr); + int sfd = accept(lfd, NULL, NULL); + TEST(sfd >= 0); + check_ping(cfd, sfd); + TEST(close(sfd) == 0); + TEST(close(cfd) == 0); + TEST(close(lfd) == 0); + } + + // accept4(fd, NULL, NULL, 0) + { + int lfd = listen_loopback(); + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); + int cfd = connect_nonblock(&addr); + int sfd = accept4(lfd, NULL, NULL, 0); + TEST(sfd >= 0); + check_ping(cfd, sfd); + TEST(close(sfd) == 0); + TEST(close(cfd) == 0); + TEST(close(lfd) == 0); + } + + // Non-null path still fills a sockaddr without crashing. + { + int lfd = listen_loopback(); + struct sockaddr_in addr; + socklen_t addr_len = sizeof(addr); + TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); + int cfd = connect_nonblock(&addr); + struct sockaddr_storage peer; + socklen_t peer_len = sizeof(peer); + int sfd = accept(lfd, (struct sockaddr *)&peer, &peer_len); + TEST(sfd >= 0); + TEST(peer_len > 0); + check_ping(cfd, sfd); + TEST(close(sfd) == 0); + TEST(close(cfd) == 0); + TEST(close(lfd) == 0); + } + + return t_status; +} From 5523609ef2ca4283b1c2acc0f6d71ec582766e6a Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 16:17:46 -0700 Subject: [PATCH 3/6] test: wait for connect before accept/send in accept-null case sockets-accept-null-addr timed out in CI: nonblocking connect was not waited on before send, so send failed with ENOTCONN (errno 53) and recv hung until the 10s CTest timeout. Match sockets-select-connecting-rw.c: select for writability and check SO_ERROR before accept and send. Signed-off-by: Sebastien Tardif --- test/src/sockets-accept-null-addr.c | 64 ++++++++++++++++++----------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/test/src/sockets-accept-null-addr.c b/test/src/sockets-accept-null-addr.c index 9235a29eb..f5e56f3b8 100644 --- a/test/src/sockets-accept-null-addr.c +++ b/test/src/sockets-accept-null-addr.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -15,29 +16,50 @@ // POSIX allows accept/accept4 peer address args to both be NULL when the // caller does not need the peer address. wasip1 previously null-dereferenced -// on that shape after a successful sock_accept. -static int listen_loopback(void) { +// after a successful sock_accept on that shape. +// +// Handshake matches sockets-select-connecting-rw.c: nonblocking connect must +// complete (select + SO_ERROR) before accept/send, or send fails with +// ENOTCONN/EAGAIN and the test hangs on recv. +static void wait_connected(int cfd) { + fd_set rfds, wfds, efds; + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_ZERO(&efds); + FD_SET(cfd, &rfds); + FD_SET(cfd, &wfds); + FD_SET(cfd, &efds); + TEST(select(cfd + 1, &rfds, &wfds, &efds, NULL) >= 0); + TEST(FD_ISSET(cfd, &wfds)); + + int err = -1; + socklen_t err_len = sizeof(err); + TEST(getsockopt(cfd, SOL_SOCKET, SO_ERROR, &err, &err_len) == 0); + TEST(err == 0); +} + +static int listen_loopback(struct sockaddr_in *addr) { int lfd = socket(AF_INET, SOCK_STREAM, 0); TEST(lfd >= 0); - struct sockaddr_in addr; - socklen_t addr_len = sizeof(addr); - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr.sin_port = 0; - TEST(bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) == 0); - TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); + socklen_t addr_len = sizeof(*addr); + memset(addr, 0, sizeof(*addr)); + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr->sin_port = 0; + TEST(bind(lfd, (struct sockaddr *)addr, sizeof(*addr)) == 0); + TEST(getsockname(lfd, (struct sockaddr *)addr, &addr_len) == 0); TEST(listen(lfd, 1) == 0); return lfd; } -static int connect_nonblock(const struct sockaddr_in *addr) { +static int connect_client(const struct sockaddr_in *addr) { int cfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); TEST(cfd >= 0); errno = 0; int rc = connect(cfd, (const struct sockaddr *)addr, sizeof(*addr)); if (rc != 0 && errno != EINPROGRESS) t_error("connect failed (rc = %d, errno = %d)\n", rc, errno); + wait_connected(cfd); return cfd; } @@ -51,11 +73,9 @@ static void check_ping(int cfd, int sfd) { int main(void) { // accept(fd, NULL, NULL) { - int lfd = listen_loopback(); struct sockaddr_in addr; - socklen_t addr_len = sizeof(addr); - TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); - int cfd = connect_nonblock(&addr); + int lfd = listen_loopback(&addr); + int cfd = connect_client(&addr); int sfd = accept(lfd, NULL, NULL); TEST(sfd >= 0); check_ping(cfd, sfd); @@ -66,11 +86,9 @@ int main(void) { // accept4(fd, NULL, NULL, 0) { - int lfd = listen_loopback(); struct sockaddr_in addr; - socklen_t addr_len = sizeof(addr); - TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); - int cfd = connect_nonblock(&addr); + int lfd = listen_loopback(&addr); + int cfd = connect_client(&addr); int sfd = accept4(lfd, NULL, NULL, 0); TEST(sfd >= 0); check_ping(cfd, sfd); @@ -79,13 +97,11 @@ int main(void) { TEST(close(lfd) == 0); } - // Non-null path still fills a sockaddr without crashing. + // Non-null peer address path still works. { - int lfd = listen_loopback(); struct sockaddr_in addr; - socklen_t addr_len = sizeof(addr); - TEST(getsockname(lfd, (struct sockaddr *)&addr, &addr_len) == 0); - int cfd = connect_nonblock(&addr); + int lfd = listen_loopback(&addr); + int cfd = connect_client(&addr); struct sockaddr_storage peer; socklen_t peer_len = sizeof(peer); int sfd = accept(lfd, (struct sockaddr *)&peer, &peer_len); From 81949586cff4d051dbdaede52dfc114361bbbd18 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 16:18:25 -0700 Subject: [PATCH 4/6] test: do not run accept-null NETWORK case on wasip1 The sockets suite is intentionally disabled for wasip1. Registering a single NETWORK accept test there can hang or fail under preview1 engines and does not match existing CI policy. Keep sockets-accept-null-addr on wasip2/p3 (where NETWORK tests already run); wasip1 accept-wasip1.c is still covered by the same source change and host red-green. Signed-off-by: Sebastien Tardif --- test/CMakeLists.txt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a0c719537..3abe16353 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -694,13 +694,6 @@ add_external_entrypoint_test(external_entrypoint_pthread_tls.c) # ========= sockets-related tests =============================== -# wasip1 skips the bulk NETWORK suite, but #866 fixed a wasip1-only -# accept(NULL) crash. Register the focused case outside the p2/p3 block so -# wasip1 CI also exercises accept-wasip1.c when the engine supports sockets. -if (WASI STREQUAL "p1" AND NOT TEST_WITH_V8) - add_wasilibc_test(sockets-accept-null-addr.c NETWORK) -endif() - if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(poll-connect.c NETWORK) add_wasilibc_test(poll-badfd.c) From a463828c30b1c1e806ffca5d0c7ace855d88860f Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 18:39:51 -0700 Subject: [PATCH 5/6] test: run accept-null regression on wasip1 The sockets suite is skipped for preview1, so registering sockets-accept-null-addr only in that block never exercised accept-wasip1.c. Register the same NETWORK case for wasip1 (non-V8) as well. Drop CMake comment-per-test notes. Signed-off-by: Sebastien Tardif --- test/CMakeLists.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3abe16353..af73d9a01 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -442,7 +442,6 @@ add_wasilibc_test(clock_nanosleep.c) add_wasilibc_test(chdir.c FS) add_wasilibc_test(close.c FS) add_wasilibc_test(pread.c FS) -# Emulated mmap free-on-pread-fail (#866). Links the optional mman library. add_wasilibc_test(mmap-pread-fail.c FS CFLAGS -D_WASI_EMULATED_MMAN LDFLAGS -lwasi-emulated-mman @@ -694,6 +693,12 @@ add_external_entrypoint_test(external_entrypoint_pthread_tls.c) # ========= sockets-related tests =============================== +# The bulk sockets suite is skipped for wasip1. Register this one case +# there so CI exercises accept-wasip1.c (the #866 crash path). +if (WASI STREQUAL "p1" AND NOT TEST_WITH_V8) + add_wasilibc_test(sockets-accept-null-addr.c NETWORK) +endif() + if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(poll-connect.c NETWORK) add_wasilibc_test(poll-badfd.c) @@ -717,7 +722,6 @@ if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(sockets-nonblocking-accept-multiple.c NETWORK) add_wasilibc_test(sockets-nonblocking-shutdown.c NETWORK) add_wasilibc_test(sockets-select-connecting-rw.c NETWORK) - # POSIX accept/accept4 with NULL peer address (#866 wasip1 crash class). add_wasilibc_test(sockets-accept-null-addr.c NETWORK) add_wasilibc_test(sockets-listen-backlog.c NETWORK) add_wasilibc_test(sockets-sendto-connected.c NETWORK) From acc8ac826f7463bc115b5b8c43049f72d714bc93 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 11 Aug 2026 18:45:10 -0700 Subject: [PATCH 6/6] test: do not compile accept-null on wasip1 wasip1 exports accept/accept4 but not socket, bind, listen, or connect, and SO_ERROR is p2/p3-only in the headers. Registering sockets-accept-null-addr for preview1 fails the C compile (-Werror implicit declarations). Keep the NETWORK case on wasip2/p3 only. Signed-off-by: Sebastien Tardif --- test/CMakeLists.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index af73d9a01..72d753cb8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -693,12 +693,6 @@ add_external_entrypoint_test(external_entrypoint_pthread_tls.c) # ========= sockets-related tests =============================== -# The bulk sockets suite is skipped for wasip1. Register this one case -# there so CI exercises accept-wasip1.c (the #866 crash path). -if (WASI STREQUAL "p1" AND NOT TEST_WITH_V8) - add_wasilibc_test(sockets-accept-null-addr.c NETWORK) -endif() - if (NOT (WASI STREQUAL "p1")) add_wasilibc_test(poll-connect.c NETWORK) add_wasilibc_test(poll-badfd.c)