Rewrite Windows connect()

Our old code wasn't working with projects like Qt that call connect() in
O_NONBLOCK mode multiple times. This change overhauls connect() to use a
simpler WSAConnect() API and follows the same pattern as cosmo accept().
This change also reduces the binary footprint of read(), which no longer
needs to depend on our enormous clock_gettime() function.
This commit is contained in:
Justine Tunney 2024-09-12 23:01:20 -07:00
parent 5469202ea8
commit e142124730
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
25 changed files with 556 additions and 277 deletions

View file

@ -34,6 +34,7 @@ LIBC_SOCK_A_DIRECTDEPS = \
LIBC_NT_IPHLPAPI \
LIBC_NT_KERNEL32 \
LIBC_NT_NTDLL \
LIBC_NT_REALTIME \
LIBC_NT_WS2_32 \
LIBC_RUNTIME \
LIBC_STDIO \

View file

@ -18,15 +18,12 @@
*/
#include "libc/calls/internal.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/errno.h"
#include "libc/intrin/kprintf.h"
#include "libc/nt/errors.h"
#include "libc/nt/struct/pollfd.h"
#include "libc/nt/thunk/msabi.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
#include "libc/sock/struct/sockaddr.h"
#include "libc/sock/syscall_fd.internal.h"
#include "libc/sysv/consts/fio.h"
#include "libc/sysv/consts/o.h"
@ -38,8 +35,6 @@
#define POLL_INTERVAL_MS 10
__msabi extern typeof(__sys_setsockopt_nt) *const __imp_setsockopt;
__msabi extern typeof(__sys_closesocket_nt) *const __imp_closesocket;
__msabi extern typeof(__sys_ioctlsocket_nt) *const __imp_ioctlsocket;
textwindows static int sys_accept_nt_impl(struct Fd *f,
@ -61,7 +56,6 @@ textwindows static int sys_accept_nt_impl(struct Fd *f,
for (;;) {
// perform non-blocking accept
// we assume listen() put f->handle in non-blocking mode
int32_t addrsize = sizeof(*addr);
struct sockaddr *paddr = (struct sockaddr *)addr;
if ((handle = WSAAccept(f->handle, paddr, &addrsize, 0, 0)) != -1)
@ -76,7 +70,7 @@ textwindows static int sys_accept_nt_impl(struct Fd *f,
return -1;
}
// we're done if user wants non-blocking
// check for non-blocking
if (f->flags & O_NONBLOCK)
return eagain();
@ -91,13 +85,6 @@ textwindows static int sys_accept_nt_impl(struct Fd *f,
return __winsockerr();
}
// inherit properties of listening socket
// errors ignored as if f->handle was created before forking
// this fails with WSAENOTSOCK, see
// https://github.com/jart/cosmopolitan/issues/1174
__imp_setsockopt(handle, SOL_SOCKET, kNtSoUpdateAcceptContext, &f->handle,
sizeof(f->handle));
// create file descriptor for new socket
// don't inherit the file open mode bits
int oflags = 0;

View file

@ -22,6 +22,9 @@
/**
* Creates client socket file descriptor for incoming connection.
*
* On Windows, when this function blocks, there may be a 10 millisecond
* delay on the handling of signals or thread cancelation.
*
* @param fd is the server socket file descriptor
* @param opt_out_addr will receive the remote address
* @param opt_inout_addrsize provides and receives addr's byte length

View file

@ -30,12 +30,23 @@
/**
* Creates client socket file descriptor for incoming connection.
*
* When `fd` is in `O_NONBLOCK` mode, this function will raise `EAGAIN`
* when no client is available to accept. To wait until a client exists
* the poll() function may be called using `POLLIN`.
*
* On Linux, your `SO_RCVTIMEO` will timeout accept4(). Other OSes (i.e.
* Windows, MacOS, and BSDs) do not support this and will block forever.
*
* On Windows, when this function blocks, there may be a 10 millisecond
* delay on the handling of signals or thread cancelation.
*
* @param fd is the server socket file descriptor
* @param opt_out_addr will receive the remote address
* @param opt_inout_addrsize provides and receives out_addr's byte length
* @param flags can have SOCK_{CLOEXEC,NONBLOCK}, which may apply to
* both the newly created socket and the server one
* @return client fd which needs close(), or -1 w/ errno
* @raise EAGAIN if `O_NONBLOCK` and no clients pending
* @cancelationpoint
* @asyncsignalsafe
* @restartable (unless SO_RCVTIMEO)

View file

@ -16,8 +16,6 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/intrin/weaken.h"
#include "libc/mem/mem.h"
#include "libc/nt/thunk/msabi.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
@ -32,9 +30,6 @@ __msabi extern typeof(__sys_closesocket_nt) *const __imp_closesocket;
* This function should only be called by close().
*/
textwindows int sys_closesocket_nt(struct Fd *f) {
if (_weaken(sys_connect_nt_cleanup)) {
_weaken(sys_connect_nt_cleanup)(f, true);
}
if (!__imp_closesocket(f->handle)) {
return 0;
} else {

View file

@ -16,92 +16,47 @@
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/assert.h"
#include "libc/atomic.h"
#include "libc/calls/internal.h"
#include "libc/calls/struct/sigset.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/cosmo.h"
#include "libc/errno.h"
#include "libc/intrin/fds.h"
#include "libc/macros.h"
#include "libc/mem/mem.h"
#include "libc/nt/enum/wsaid.h"
#include "libc/nt/errors.h"
#include "libc/nt/struct/guid.h"
#include "libc/nt/struct/overlapped.h"
#include "libc/nt/thread.h"
#include "libc/nt/struct/fdset.h"
#include "libc/nt/struct/pollfd.h"
#include "libc/nt/struct/timeval.h"
#include "libc/nt/thunk/msabi.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
#include "libc/sock/struct/sockaddr.h"
#include "libc/sock/syscall_fd.internal.h"
#include "libc/sock/wsaid.internal.h"
#include "libc/sysv/consts/af.h"
#include "libc/sysv/consts/fio.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/poll.h"
#include "libc/sysv/consts/so.h"
#include "libc/sysv/consts/sol.h"
#include "libc/sysv/errfuns.h"
#ifdef __x86_64__
#include "libc/sock/yoink.inc"
__msabi extern typeof(__sys_setsockopt_nt) *const __imp_setsockopt;
#define UNCONNECTED 0
#define CONNECTING 1
#define CONNECTED 2
struct ConnectArgs {
const void *addr;
uint32_t addrsize;
};
#define POLL_INTERVAL_MS 10
static struct {
atomic_uint once;
bool32 (*__msabi lpConnectEx)(int64_t hSocket, const struct sockaddr *name,
int namelen, const void *opt_lpSendBuffer,
uint32_t dwSendDataLength,
uint32_t *opt_out_lpdwBytesSent,
struct NtOverlapped *lpOverlapped);
} g_connectex;
__msabi extern typeof(__sys_getsockopt_nt) *const __imp_getsockopt;
__msabi extern typeof(__sys_ioctlsocket_nt) *const __imp_ioctlsocket;
__msabi extern typeof(__sys_select_nt) *const __imp_select;
static void connectex_init(void) {
static struct NtGuid ConnectExGuid = WSAID_CONNECTEX;
g_connectex.lpConnectEx = __get_wsaid(&ConnectExGuid);
}
textwindows static int sys_connect_nt_impl(struct Fd *f, const void *addr,
uint32_t addrsize,
sigset_t waitmask) {
void sys_connect_nt_cleanup(struct Fd *f, bool cancel) {
struct NtOverlapped *overlap;
if ((overlap = f->connect_op)) {
uint32_t got, flags;
if (cancel)
CancelIoEx(f->handle, overlap);
if (WSAGetOverlappedResult(f->handle, overlap, &got, cancel, &flags) ||
WSAGetLastError() != kNtErrorIoIncomplete) {
WSACloseEvent(overlap->hEvent);
free(overlap);
f->connect_op = 0;
}
}
}
// check if already connected
if (f->connecting == 2)
return eisconn();
static int sys_connect_nt_start(int64_t hSocket,
struct NtOverlapped *lpOverlapped,
uint32_t *flags, void *arg) {
struct ConnectArgs *args = arg;
if (g_connectex.lpConnectEx(hSocket, args->addr, args->addrsize, 0, 0, 0,
lpOverlapped)) {
return 0;
} else {
return -1;
}
}
static textwindows int sys_connect_nt_impl(struct Fd *f, const void *addr,
uint32_t addrsize, sigset_t mask) {
// get connect function from winsock api
cosmo_once(&g_connectex.once, connectex_init);
// fail if previous connect() is still in progress
if (f->connect_op)
return ealready();
// ConnectEx() requires bind() be called beforehand
// winsock requires bind() be called beforehand
if (!f->isbound) {
struct sockaddr_storage ss = {0};
ss.ss_family = ((struct sockaddr *)addr)->sa_family;
@ -109,60 +64,121 @@ static textwindows int sys_connect_nt_impl(struct Fd *f, const void *addr,
return -1;
}
// perform normal connect
if (!(f->flags & O_NONBLOCK)) {
f->peer.ss_family = AF_UNSPEC;
ssize_t rc = __winsock_block(f->handle, 0, false, f->sndtimeo, mask,
sys_connect_nt_start,
&(struct ConnectArgs){addr, addrsize});
if (rc == -1 && errno == EAGAIN) {
// return ETIMEDOUT if SO_SNDTIMEO elapsed
// note that Linux will return EINPROGRESS
errno = etimedout();
} else if (!rc) {
__imp_setsockopt(f->handle, SOL_SOCKET, kNtSoUpdateConnectContext, 0, 0);
if (f->connecting == UNCONNECTED) {
// make sure winsock is in non-blocking mode
uint32_t mode = 1;
if (__imp_ioctlsocket(f->handle, FIONBIO, &mode))
return __winsockerr();
// perform non-blocking connect
if (!WSAConnect(f->handle, addr, addrsize, 0, 0, 0, 0)) {
f->connecting = CONNECTED;
return 0;
}
// check for errors
switch (WSAGetLastError()) {
case WSAEISCONN:
f->connecting = CONNECTED;
return eisconn();
case WSAEALREADY:
f->connecting = CONNECTING;
break;
case WSAEWOULDBLOCK:
break;
default:
return __winsockerr();
}
// handle non-blocking
if (f->flags & O_NONBLOCK) {
if (f->connecting == UNCONNECTED) {
f->connecting = CONNECTING;
return einprogress();
} else {
return ealready();
}
} else {
f->connecting = CONNECTING;
}
return rc;
}
// win32 getpeername() stops working in non-blocking connect mode
if (addrsize)
memcpy(&f->peer, addr, MIN(addrsize, sizeof(struct sockaddr_storage)));
for (;;) {
// perform nonblocking connect(), i.e.
// 1. connect(O_NONBLOCK) → EINPROGRESS
// 2. poll(POLLOUT)
bool32 ok;
struct NtOverlapped *overlap = calloc(1, sizeof(struct NtOverlapped));
if (!overlap)
return -1;
overlap->hEvent = WSACreateEvent();
ok = g_connectex.lpConnectEx(f->handle, addr, addrsize, 0, 0, 0, overlap);
if (ok) {
uint32_t dwBytes, dwFlags;
ok = WSAGetOverlappedResult(f->handle, overlap, &dwBytes, false, &dwFlags);
WSACloseEvent(overlap->hEvent);
free(overlap);
if (!ok) {
return __winsockerr();
// check for signals and thread cancelation
// connect() will restart if SA_RESTART is used
if (!(f->flags & O_NONBLOCK))
if (__sigcheck(waitmask, true) == -1)
return -1;
//
// "Use select to determine the completion of the connection request
// by checking if the socket is writable."
//
// —Quoth MSDN § WSAConnect function
//
// "If a socket is processing a connect call (nonblocking), failure
// of the connect attempt is indicated in exceptfds (application
// must then call getsockopt SO_ERROR to determine the error value
// to describe why the failure occurred). This document does not
// define which other errors will be included."
//
// —Quoth MSDN § select function
//
struct NtFdSet wrfds;
struct NtFdSet exfds;
struct NtTimeval timeout;
wrfds.fd_count = 1;
wrfds.fd_array[0] = f->handle;
exfds.fd_count = 1;
exfds.fd_array[0] = f->handle;
if (f->flags & O_NONBLOCK) {
timeout.tv_sec = 0;
timeout.tv_usec = 0;
} else {
timeout.tv_sec = POLL_INTERVAL_MS / 1000;
timeout.tv_usec = POLL_INTERVAL_MS % 1000 * 1000;
}
__imp_setsockopt(f->handle, SOL_SOCKET, kNtSoUpdateConnectContext, 0, 0);
int ready = __imp_select(1, 0, &wrfds, &exfds, &timeout);
if (ready == -1)
return __winsockerr();
// check if we still need more time
if (!ready) {
if (f->flags & O_NONBLOCK) {
return ealready();
} else {
continue;
}
}
// check if connect failed
if (exfds.fd_count) {
int err;
uint32_t len = sizeof(err);
if (__imp_getsockopt(f->handle, SOL_SOCKET, SO_ERROR, &err, &len) == -1)
return __winsockerr();
if (!err)
return eio(); // should be impossible
errno = __dos2errno(err);
return -1;
}
// handle successful connection
if (!wrfds.fd_count)
return eio(); // should be impossible
f->connecting = CONNECTED;
return 0;
} else if (WSAGetLastError() == kNtErrorIoPending) {
f->connect_op = overlap;
return einprogress();
} else {
WSACloseEvent(overlap->hEvent);
free(overlap);
return __winsockerr();
}
}
textwindows int sys_connect_nt(struct Fd *f, const void *addr,
uint32_t addrsize) {
sigset_t mask = __sig_block();
int rc = sys_connect_nt_impl(f, addr, addrsize, mask);
__sig_unblock(mask);
int rc;
BLOCK_SIGNALS;
rc = sys_connect_nt_impl(f, addr, addrsize, _SigMask);
ALLOW_SIGNALS;
return rc;
}

View file

@ -31,12 +31,23 @@
/**
* Connects socket to remote end.
*
* ProTip: Connectionless sockets, e.g. UDP, can be connected too. The
* benefit is not needing to specify the remote address on each send. It
* also means getsockname() can be called to retrieve routing details.
* When `fd` is in `O_NONBLOCK` mode, this raises `EINPROGRESS`. To wait
* for establishment poll() function may be called using `POLLOUT`. Then
* `SO_ERROR` may be used to check for errors.
*
* Connectionless sockets, e.g. UDP, can be connected too. The benefit
* is not needing to specify the remote address on each send. It also
* means getsockname() can be called to retrieve routing details.
*
* On Linux, your `SO_SNDTIMEO` will timeout connect(). Other OSes (i.e.
* Windows, MacOS, and BSDs) do not support this and will block forever.
*
* On Windows, when this function blocks, there may be a 10 millisecond
* delay on the handling of signals or thread cancelation.
*
* @return 0 on success or -1 w/ errno
* @raise EALREADY if a non-blocking connection request already happened
* @raise EINPROGRESS if `O_NONBLOCK` and connecting process initiated
* @raise EALREADY if a `O_NONBLOCK` connecting already in flight
* @raise EADDRINUSE if local address is already in use
* @raise EINTR if a signal handler was called instead
* @raise ENETUNREACH if network is unreachable

View file

@ -46,12 +46,7 @@ static int __getsockpeername(int fd, struct sockaddr *out_addr,
if (IsWindows()) {
if (__isfdkind(fd, kFdSocket)) {
if ((rc = impl_win32(g_fds.p[fd].handle, &ss, &size))) {
if (impl_win32 == __imp_getpeername &&
g_fds.p[fd].peer.ss_family != AF_UNSPEC) {
ss = g_fds.p[fd].peer;
rc = 0;
} else if (impl_win32 == __imp_getsockname &&
WSAGetLastError() == WSAEINVAL) {
if (impl_win32 == __imp_getsockname && WSAGetLastError() == WSAEINVAL) {
// The socket has not been bound to an address with bind, or
// ADDR_ANY is specified in bind but connection has not yet
// occurred. -MSDN

View file

@ -47,6 +47,19 @@ textwindows int sys_getsockopt_nt(struct Fd *fd, int level, int optname,
in_optlen = 0;
}
if (level == SOL_SOCKET && optname == SO_ERROR) {
if (in_optlen >= sizeof(int)) {
int err;
uint32_t len = sizeof(err);
if (__imp_getsockopt(fd->handle, SOL_SOCKET, SO_ERROR, &err, &len) == -1)
return __winsockerr();
*(int *)out_opt_optval = __dos2errno(err);
*inout_optlen = sizeof(int);
} else {
return einval();
}
}
if (level == SOL_SOCKET &&
(optname == SO_RCVTIMEO || optname == SO_SNDTIMEO)) {
if (in_optlen >= sizeof(struct timeval)) {

View file

@ -17,9 +17,9 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/internal.h"
#include "libc/intrin/fds.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/intrin/fds.h"
#include "libc/nt/struct/iovec.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
@ -50,9 +50,8 @@ static textwindows int sys_recv_nt_start(int64_t handle,
textwindows ssize_t sys_recv_nt(int fd, const struct iovec *iov, size_t iovlen,
uint32_t flags) {
if (flags & ~(_MSG_DONTWAIT | _MSG_OOB | _MSG_PEEK | _MSG_WAITALL)) {
if (flags & ~(_MSG_DONTWAIT | _MSG_OOB | _MSG_PEEK | _MSG_WAITALL))
return einval();
}
ssize_t rc;
struct Fd *f = g_fds.p + fd;
sigset_t m = __sig_block();

View file

@ -17,9 +17,9 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/internal.h"
#include "libc/intrin/fds.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/intrin/fds.h"
#include "libc/nt/struct/iovec.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"

View file

@ -17,9 +17,9 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/internal.h"
#include "libc/intrin/fds.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/intrin/fds.h"
#include "libc/nt/struct/iovec.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"

View file

@ -38,6 +38,7 @@
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
#include "libc/sock/sendfile.internal.h"
#include "libc/sock/syscall_fd.internal.h"
#include "libc/sock/wsaid.internal.h"
#include "libc/stdio/sysparam.h"
#include "libc/sysv/errfuns.h"
@ -58,7 +59,7 @@ static void transmitfile_init(void) {
g_transmitfile.lpTransmitFile = __get_wsaid(&TransmitfileGuid);
}
static dontinline textwindows ssize_t sys_sendfile_nt(
textwindows dontinline static ssize_t sys_sendfile_nt(
int outfd, int infd, int64_t *opt_in_out_inoffset, uint32_t uptobytes) {
ssize_t rc;
uint32_t flags = 0;

View file

@ -17,9 +17,9 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/internal.h"
#include "libc/intrin/fds.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/sigset.internal.h"
#include "libc/intrin/fds.h"
#include "libc/nt/struct/iovec.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"

View file

@ -6,7 +6,6 @@
#include "libc/sock/struct/sockaddr.h"
COSMOPOLITAN_C_START_
void sys_connect_nt_cleanup(struct Fd *, bool);
int sys_accept_nt(struct Fd *, struct sockaddr_storage *, int);
int sys_bind_nt(struct Fd *, const void *, uint32_t);
int sys_closesocket_nt(struct Fd *);